Compare commits
34 Commits
master
...
content_de
Author | SHA1 | Date | |
---|---|---|---|
87f7290301 | |||
7ebbad2b94 | |||
5b3e0e2a0b | |||
5eca1a99e0 | |||
d0eeef2b94 | |||
4d5d708d6d | |||
9b7ba5875c | |||
9b5cb2cfab | |||
9f62e01ab8 | |||
8cdf2a2ca3 | |||
697611ff55 | |||
42c7ab8571 | |||
36e75c0fab | |||
a934273714 | |||
a618435f17 | |||
a622b6aa3f | |||
b4373e0d9a | |||
964f6de656 | |||
a100eaae82 | |||
106c8e8403 | |||
ca4ab01f77 | |||
4ff9386398 | |||
93d65ead89 | |||
edd949879b | |||
ef78c49e29 | |||
e149873673 | |||
3a98e10007 | |||
b3e5e4c950 | |||
165e80c456 | |||
495ec41234 | |||
ddadc9bdbc | |||
b657802e8d | |||
bedf0b02bc | |||
1d0a4cafe2 |
@ -27,11 +27,19 @@ message("II TOMATO_TOX_AV: ${TOMATO_TOX_AV}")
|
|||||||
if (TOMATO_ASAN)
|
if (TOMATO_ASAN)
|
||||||
if (${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU" OR ${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang")
|
if (${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU" OR ${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang")
|
||||||
if (NOT WIN32) # exclude mingw
|
if (NOT WIN32) # exclude mingw
|
||||||
#link_libraries(-fsanitize=address)
|
add_compile_options(-fsanitize=address,undefined)
|
||||||
link_libraries(-fsanitize=address,undefined)
|
link_libraries(-fsanitize=address,undefined)
|
||||||
#link_libraries(-fsanitize=undefined)
|
#link_libraries(-fsanitize=undefined)
|
||||||
link_libraries(-static-libasan) # make it "work" on nix
|
link_libraries(-static-libasan) # make it "work" on nix
|
||||||
|
|
||||||
|
#add_compile_options(-fsanitize=thread)
|
||||||
|
#link_libraries(-fsanitize=thread)
|
||||||
|
|
||||||
message("II enabled ASAN")
|
message("II enabled ASAN")
|
||||||
|
if (OFF) # TODO: switch for minimal runtime in deployed scenarios
|
||||||
|
add_compile_options(-fsanitize-minimal-runtime)
|
||||||
|
link_libraries(-fsanitize-minimal-runtime)
|
||||||
|
endif()
|
||||||
else()
|
else()
|
||||||
message("!! can not enable ASAN on this platform (gcc/clang + win)")
|
message("!! can not enable ASAN on this platform (gcc/clang + win)")
|
||||||
endif()
|
endif()
|
||||||
|
1
external/CMakeLists.txt
vendored
1
external/CMakeLists.txt
vendored
@ -24,3 +24,4 @@ add_subdirectory(./libwebp)
|
|||||||
add_subdirectory(./qoi)
|
add_subdirectory(./qoi)
|
||||||
add_subdirectory(./sdl_image)
|
add_subdirectory(./sdl_image)
|
||||||
|
|
||||||
|
add_subdirectory(./spscqueue)
|
||||||
|
9
external/spscqueue/CMakeLists.txt
vendored
Normal file
9
external/spscqueue/CMakeLists.txt
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.9 FATAL_ERROR)
|
||||||
|
|
||||||
|
add_library(SPSCQueue INTERFACE
|
||||||
|
./SPSCQueue.h
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_features(SPSCQueue INTERFACE cxx_std_17)
|
||||||
|
target_include_directories(SPSCQueue INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||||
|
|
237
external/spscqueue/SPSCQueue.h
vendored
Normal file
237
external/spscqueue/SPSCQueue.h
vendored
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) 2020 Erik Rigtorp <erik@rigtorp.se>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <cassert>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <memory> // std::allocator
|
||||||
|
#include <new> // std::hardware_destructive_interference_size
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <type_traits> // std::enable_if, std::is_*_constructible
|
||||||
|
|
||||||
|
#ifdef __has_cpp_attribute
|
||||||
|
#if __has_cpp_attribute(nodiscard)
|
||||||
|
#define RIGTORP_NODISCARD [[nodiscard]]
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
#ifndef RIGTORP_NODISCARD
|
||||||
|
#define RIGTORP_NODISCARD
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace rigtorp {
|
||||||
|
|
||||||
|
template <typename T, typename Allocator = std::allocator<T>> class SPSCQueue {
|
||||||
|
|
||||||
|
#if defined(__cpp_if_constexpr) && defined(__cpp_lib_void_t)
|
||||||
|
template <typename Alloc2, typename = void>
|
||||||
|
struct has_allocate_at_least : std::false_type {};
|
||||||
|
|
||||||
|
template <typename Alloc2>
|
||||||
|
struct has_allocate_at_least<
|
||||||
|
Alloc2, std::void_t<typename Alloc2::value_type,
|
||||||
|
decltype(std::declval<Alloc2 &>().allocate_at_least(
|
||||||
|
size_t{}))>> : std::true_type {};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit SPSCQueue(const size_t capacity,
|
||||||
|
const Allocator &allocator = Allocator())
|
||||||
|
: capacity_(capacity), allocator_(allocator) {
|
||||||
|
// The queue needs at least one element
|
||||||
|
if (capacity_ < 1) {
|
||||||
|
capacity_ = 1;
|
||||||
|
}
|
||||||
|
capacity_++; // Needs one slack element
|
||||||
|
// Prevent overflowing size_t
|
||||||
|
if (capacity_ > SIZE_MAX - 2 * kPadding) {
|
||||||
|
capacity_ = SIZE_MAX - 2 * kPadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(__cpp_if_constexpr) && defined(__cpp_lib_void_t)
|
||||||
|
if constexpr (has_allocate_at_least<Allocator>::value) {
|
||||||
|
auto res = allocator_.allocate_at_least(capacity_ + 2 * kPadding);
|
||||||
|
slots_ = res.ptr;
|
||||||
|
capacity_ = res.count - 2 * kPadding;
|
||||||
|
} else {
|
||||||
|
slots_ = std::allocator_traits<Allocator>::allocate(
|
||||||
|
allocator_, capacity_ + 2 * kPadding);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
slots_ = std::allocator_traits<Allocator>::allocate(
|
||||||
|
allocator_, capacity_ + 2 * kPadding);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static_assert(alignof(SPSCQueue<T>) == kCacheLineSize, "");
|
||||||
|
static_assert(sizeof(SPSCQueue<T>) >= 3 * kCacheLineSize, "");
|
||||||
|
assert(reinterpret_cast<char *>(&readIdx_) -
|
||||||
|
reinterpret_cast<char *>(&writeIdx_) >=
|
||||||
|
static_cast<std::ptrdiff_t>(kCacheLineSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
~SPSCQueue() {
|
||||||
|
while (front()) {
|
||||||
|
pop();
|
||||||
|
}
|
||||||
|
std::allocator_traits<Allocator>::deallocate(allocator_, slots_,
|
||||||
|
capacity_ + 2 * kPadding);
|
||||||
|
}
|
||||||
|
|
||||||
|
// non-copyable and non-movable
|
||||||
|
SPSCQueue(const SPSCQueue &) = delete;
|
||||||
|
SPSCQueue &operator=(const SPSCQueue &) = delete;
|
||||||
|
|
||||||
|
template <typename... Args>
|
||||||
|
void emplace(Args &&...args) noexcept(
|
||||||
|
std::is_nothrow_constructible<T, Args &&...>::value) {
|
||||||
|
static_assert(std::is_constructible<T, Args &&...>::value,
|
||||||
|
"T must be constructible with Args&&...");
|
||||||
|
auto const writeIdx = writeIdx_.load(std::memory_order_relaxed);
|
||||||
|
auto nextWriteIdx = writeIdx + 1;
|
||||||
|
if (nextWriteIdx == capacity_) {
|
||||||
|
nextWriteIdx = 0;
|
||||||
|
}
|
||||||
|
while (nextWriteIdx == readIdxCache_) {
|
||||||
|
readIdxCache_ = readIdx_.load(std::memory_order_acquire);
|
||||||
|
}
|
||||||
|
new (&slots_[writeIdx + kPadding]) T(std::forward<Args>(args)...);
|
||||||
|
writeIdx_.store(nextWriteIdx, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename... Args>
|
||||||
|
RIGTORP_NODISCARD bool try_emplace(Args &&...args) noexcept(
|
||||||
|
std::is_nothrow_constructible<T, Args &&...>::value) {
|
||||||
|
static_assert(std::is_constructible<T, Args &&...>::value,
|
||||||
|
"T must be constructible with Args&&...");
|
||||||
|
auto const writeIdx = writeIdx_.load(std::memory_order_relaxed);
|
||||||
|
auto nextWriteIdx = writeIdx + 1;
|
||||||
|
if (nextWriteIdx == capacity_) {
|
||||||
|
nextWriteIdx = 0;
|
||||||
|
}
|
||||||
|
if (nextWriteIdx == readIdxCache_) {
|
||||||
|
readIdxCache_ = readIdx_.load(std::memory_order_acquire);
|
||||||
|
if (nextWriteIdx == readIdxCache_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
new (&slots_[writeIdx + kPadding]) T(std::forward<Args>(args)...);
|
||||||
|
writeIdx_.store(nextWriteIdx, std::memory_order_release);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void push(const T &v) noexcept(std::is_nothrow_copy_constructible<T>::value) {
|
||||||
|
static_assert(std::is_copy_constructible<T>::value,
|
||||||
|
"T must be copy constructible");
|
||||||
|
emplace(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename P, typename = typename std::enable_if<
|
||||||
|
std::is_constructible<T, P &&>::value>::type>
|
||||||
|
void push(P &&v) noexcept(std::is_nothrow_constructible<T, P &&>::value) {
|
||||||
|
emplace(std::forward<P>(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
RIGTORP_NODISCARD bool
|
||||||
|
try_push(const T &v) noexcept(std::is_nothrow_copy_constructible<T>::value) {
|
||||||
|
static_assert(std::is_copy_constructible<T>::value,
|
||||||
|
"T must be copy constructible");
|
||||||
|
return try_emplace(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename P, typename = typename std::enable_if<
|
||||||
|
std::is_constructible<T, P &&>::value>::type>
|
||||||
|
RIGTORP_NODISCARD bool
|
||||||
|
try_push(P &&v) noexcept(std::is_nothrow_constructible<T, P &&>::value) {
|
||||||
|
return try_emplace(std::forward<P>(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
RIGTORP_NODISCARD T *front() noexcept {
|
||||||
|
auto const readIdx = readIdx_.load(std::memory_order_relaxed);
|
||||||
|
if (readIdx == writeIdxCache_) {
|
||||||
|
writeIdxCache_ = writeIdx_.load(std::memory_order_acquire);
|
||||||
|
if (writeIdxCache_ == readIdx) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &slots_[readIdx + kPadding];
|
||||||
|
}
|
||||||
|
|
||||||
|
void pop() noexcept {
|
||||||
|
static_assert(std::is_nothrow_destructible<T>::value,
|
||||||
|
"T must be nothrow destructible");
|
||||||
|
auto const readIdx = readIdx_.load(std::memory_order_relaxed);
|
||||||
|
assert(writeIdx_.load(std::memory_order_acquire) != readIdx &&
|
||||||
|
"Can only call pop() after front() has returned a non-nullptr");
|
||||||
|
slots_[readIdx + kPadding].~T();
|
||||||
|
auto nextReadIdx = readIdx + 1;
|
||||||
|
if (nextReadIdx == capacity_) {
|
||||||
|
nextReadIdx = 0;
|
||||||
|
}
|
||||||
|
readIdx_.store(nextReadIdx, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
RIGTORP_NODISCARD size_t size() const noexcept {
|
||||||
|
std::ptrdiff_t diff = writeIdx_.load(std::memory_order_acquire) -
|
||||||
|
readIdx_.load(std::memory_order_acquire);
|
||||||
|
if (diff < 0) {
|
||||||
|
diff += capacity_;
|
||||||
|
}
|
||||||
|
return static_cast<size_t>(diff);
|
||||||
|
}
|
||||||
|
|
||||||
|
RIGTORP_NODISCARD bool empty() const noexcept {
|
||||||
|
return writeIdx_.load(std::memory_order_acquire) ==
|
||||||
|
readIdx_.load(std::memory_order_acquire);
|
||||||
|
}
|
||||||
|
|
||||||
|
RIGTORP_NODISCARD size_t capacity() const noexcept { return capacity_ - 1; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
#ifdef __cpp_lib_hardware_interference_size
|
||||||
|
static constexpr size_t kCacheLineSize =
|
||||||
|
std::hardware_destructive_interference_size;
|
||||||
|
#else
|
||||||
|
static constexpr size_t kCacheLineSize = 64;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Padding to avoid false sharing between slots_ and adjacent allocations
|
||||||
|
static constexpr size_t kPadding = (kCacheLineSize - 1) / sizeof(T) + 1;
|
||||||
|
|
||||||
|
private:
|
||||||
|
size_t capacity_;
|
||||||
|
T *slots_;
|
||||||
|
#if defined(__has_cpp_attribute) && __has_cpp_attribute(no_unique_address)
|
||||||
|
Allocator allocator_ [[no_unique_address]];
|
||||||
|
#else
|
||||||
|
Allocator allocator_;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Align to cache line size in order to avoid false sharing
|
||||||
|
// readIdxCache_ and writeIdxCache_ is used to reduce the amount of cache
|
||||||
|
// coherency traffic
|
||||||
|
alignas(kCacheLineSize) std::atomic<size_t> writeIdx_ = {0};
|
||||||
|
alignas(kCacheLineSize) size_t readIdxCache_ = 0;
|
||||||
|
alignas(kCacheLineSize) std::atomic<size_t> readIdx_ = {0};
|
||||||
|
alignas(kCacheLineSize) size_t writeIdxCache_ = 0;
|
||||||
|
};
|
||||||
|
} // namespace rigtorp
|
@ -80,6 +80,8 @@
|
|||||||
] ++ self.packages.${system}.default.dlopenBuildInputs;
|
] ++ self.packages.${system}.default.dlopenBuildInputs;
|
||||||
|
|
||||||
cmakeFlags = [
|
cmakeFlags = [
|
||||||
|
"-DTOMATO_TOX_AV=ON"
|
||||||
|
|
||||||
"-DTOMATO_ASAN=OFF"
|
"-DTOMATO_ASAN=OFF"
|
||||||
"-DCMAKE_BUILD_TYPE=RelWithDebInfo"
|
"-DCMAKE_BUILD_TYPE=RelWithDebInfo"
|
||||||
#"-DCMAKE_BUILD_TYPE=Debug"
|
#"-DCMAKE_BUILD_TYPE=Debug"
|
||||||
|
@ -102,12 +102,30 @@ target_sources(tomato PUBLIC
|
|||||||
|
|
||||||
./chat_gui4.hpp
|
./chat_gui4.hpp
|
||||||
./chat_gui4.cpp
|
./chat_gui4.cpp
|
||||||
|
|
||||||
|
./stream_manager.hpp
|
||||||
|
./stream_manager_ui.hpp
|
||||||
|
./stream_manager_ui.cpp
|
||||||
|
|
||||||
|
./debug_video_tap.hpp
|
||||||
|
./debug_video_tap.cpp
|
||||||
|
|
||||||
|
./content/content.hpp
|
||||||
|
./content/frame_stream2.hpp
|
||||||
|
./content/sdl_video_frame_stream2.hpp
|
||||||
|
./content/sdl_video_frame_stream2.cpp
|
||||||
|
./content/audio_stream.hpp
|
||||||
|
./content/sdl_audio_frame_stream2.hpp
|
||||||
|
./content/sdl_audio_frame_stream2.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
if (TOMATO_TOX_AV)
|
if (TOMATO_TOX_AV)
|
||||||
target_sources(tomato PUBLIC
|
target_sources(tomato PUBLIC
|
||||||
./tox_av.hpp
|
./tox_av.hpp
|
||||||
./tox_av.cpp
|
./tox_av.cpp
|
||||||
|
|
||||||
|
./debug_tox_call.hpp
|
||||||
|
./debug_tox_call.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_definitions(tomato PUBLIC TOMATO_TOX_AV)
|
target_compile_definitions(tomato PUBLIC TOMATO_TOX_AV)
|
||||||
@ -140,6 +158,8 @@ target_link_libraries(tomato PUBLIC
|
|||||||
libwebpmux # the f why (needed for anim encode)
|
libwebpmux # the f why (needed for anim encode)
|
||||||
qoi
|
qoi
|
||||||
SDL3_image::SDL3_image
|
SDL3_image::SDL3_image
|
||||||
|
|
||||||
|
SPSCQueue
|
||||||
)
|
)
|
||||||
|
|
||||||
# probably not enough
|
# probably not enough
|
||||||
|
72
src/content/audio_stream.hpp
Normal file
72
src/content/audio_stream.hpp
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "./frame_stream2.hpp"
|
||||||
|
|
||||||
|
#include <solanaceae/util/span.hpp>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <variant>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// raw audio
|
||||||
|
// channels make samples interleaved,
|
||||||
|
// planar channels are not supported
|
||||||
|
struct AudioFrame {
|
||||||
|
// sequence number, to detect gaps
|
||||||
|
uint32_t seq {0};
|
||||||
|
// TODO: maybe use ts instead to discard old?
|
||||||
|
// since buffer size is variable, some timestamp would be needed to estimate the lost time
|
||||||
|
|
||||||
|
// samples per second
|
||||||
|
uint32_t sample_rate {48'000};
|
||||||
|
|
||||||
|
size_t channels {0};
|
||||||
|
std::variant<
|
||||||
|
std::vector<int16_t>, // S16, platform endianess
|
||||||
|
Span<int16_t>, // non owning variant, for direct consumption
|
||||||
|
|
||||||
|
std::vector<float>, // f32
|
||||||
|
Span<float> // non owning variant, for direct consumption
|
||||||
|
> buffer;
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
|
||||||
|
bool isS16(void) const {
|
||||||
|
return
|
||||||
|
std::holds_alternative<std::vector<int16_t>>(buffer) ||
|
||||||
|
std::holds_alternative<Span<int16_t>>(buffer)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
bool isF32(void) const {
|
||||||
|
return
|
||||||
|
std::holds_alternative<std::vector<float>>(buffer) ||
|
||||||
|
std::holds_alternative<Span<float>>(buffer)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
template<typename T>
|
||||||
|
Span<T> getSpan(void) const {
|
||||||
|
static_assert(std::is_same_v<int16_t, T> || std::is_same_v<float, T>);
|
||||||
|
if constexpr (std::is_same_v<int16_t, T>) {
|
||||||
|
assert(isS16());
|
||||||
|
if (std::holds_alternative<std::vector<int16_t>>(buffer)) {
|
||||||
|
return Span<int16_t>{std::get<std::vector<int16_t>>(buffer)};
|
||||||
|
} else {
|
||||||
|
return std::get<Span<int16_t>>(buffer);
|
||||||
|
}
|
||||||
|
} else if constexpr (std::is_same_v<float, T>) {
|
||||||
|
assert(isF32());
|
||||||
|
if (std::holds_alternative<std::vector<float>>(buffer)) {
|
||||||
|
return Span<float>{std::get<std::vector<float>>(buffer)};
|
||||||
|
} else {
|
||||||
|
return std::get<Span<float>>(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using AudioFrameStream2I = FrameStream2I<AudioFrame>;
|
||||||
|
|
||||||
|
using AudioFrameStream2MultiSource = FrameStream2MultiSource<AudioFrame>;
|
||||||
|
using AudioFrameStream2 = AudioFrameStream2MultiSource::sub_stream_type_t; // just use the default for now
|
||||||
|
|
36
src/content/content.hpp
Normal file
36
src/content/content.hpp
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <entt/container/dense_set.hpp>
|
||||||
|
|
||||||
|
#include <solanaceae/object_store/object_store.hpp>
|
||||||
|
#include <solanaceae/contact/contact_model3.hpp>
|
||||||
|
#include <solanaceae/message3/registry_message_model.hpp>
|
||||||
|
|
||||||
|
#include <solanaceae/file/file2.hpp>
|
||||||
|
|
||||||
|
namespace Content1::Components {
|
||||||
|
|
||||||
|
// TODO: design it as a tree?
|
||||||
|
|
||||||
|
// or something
|
||||||
|
struct TagFile {};
|
||||||
|
struct TagAudioStream {};
|
||||||
|
struct TagVideoStream {};
|
||||||
|
|
||||||
|
struct TimingTiedTo {
|
||||||
|
entt::dense_set<ObjectHandle> ties;
|
||||||
|
};
|
||||||
|
|
||||||
|
// the associated messages, if any
|
||||||
|
// useful if you want to update progress on the message
|
||||||
|
struct Messages {
|
||||||
|
std::vector<Message3Handle> messages;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ?
|
||||||
|
struct SuspectedParticipants {
|
||||||
|
entt::dense_set<Contact3> participants;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // Content::Components
|
||||||
|
|
153
src/content/frame_stream2.hpp
Normal file
153
src/content/frame_stream2.hpp
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include <SPSCQueue.h>
|
||||||
|
|
||||||
|
// Frames ofen consist of:
|
||||||
|
// - seq id // incremental sequential id, gaps in ids can be used to detect loss
|
||||||
|
// - data // the frame data
|
||||||
|
// eg:
|
||||||
|
//struct ExampleFrame {
|
||||||
|
//int64_t seq_id {0};
|
||||||
|
//std::vector<uint8_t> data;
|
||||||
|
//};
|
||||||
|
|
||||||
|
template<typename FrameType>
|
||||||
|
struct FrameStream2I {
|
||||||
|
virtual ~FrameStream2I(void) {}
|
||||||
|
|
||||||
|
// get number of available frames
|
||||||
|
[[nodiscard]] virtual int32_t size(void) = 0;
|
||||||
|
|
||||||
|
// get next frame
|
||||||
|
// TODO: optional instead?
|
||||||
|
// data sharing? -> no, data is copied for each fsr, if concurency supported
|
||||||
|
[[nodiscard]] virtual std::optional<FrameType> pop(void) = 0;
|
||||||
|
|
||||||
|
// returns true if there are readers (or we dont know)
|
||||||
|
virtual bool push(const FrameType& value) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename FrameType>
|
||||||
|
struct FrameStream2SourceI {
|
||||||
|
virtual ~FrameStream2SourceI(void) {}
|
||||||
|
[[nodiscard]] virtual std::shared_ptr<FrameStream2I<FrameType>> subscribe(void) = 0;
|
||||||
|
virtual bool unsubscribe(const std::shared_ptr<FrameStream2I<FrameType>>& sub) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename FrameType>
|
||||||
|
struct FrameStream2SinkI {
|
||||||
|
virtual ~FrameStream2SinkI(void) {}
|
||||||
|
[[nodiscard]] virtual std::shared_ptr<FrameStream2I<FrameType>> subscribe(void) = 0;
|
||||||
|
virtual bool unsubscribe(const std::shared_ptr<FrameStream2I<FrameType>>& sub) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// needs count frames queue size
|
||||||
|
// having ~1-2sec buffer size is often sufficent
|
||||||
|
template<typename FrameType>
|
||||||
|
struct QueuedFrameStream2 : public FrameStream2I<FrameType> {
|
||||||
|
using frame_type = FrameType;
|
||||||
|
|
||||||
|
rigtorp::SPSCQueue<FrameType> _queue;
|
||||||
|
|
||||||
|
// discard values if queue full
|
||||||
|
// will block if not lossy and full on push
|
||||||
|
const bool _lossy {true};
|
||||||
|
|
||||||
|
explicit QueuedFrameStream2(size_t queue_size, bool lossy = true) : _queue(queue_size), _lossy(lossy) {}
|
||||||
|
|
||||||
|
int32_t size(void) override {
|
||||||
|
return _queue.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<FrameType> pop(void) override {
|
||||||
|
auto* ret_ptr = _queue.front();
|
||||||
|
if (ret_ptr == nullptr) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// move away
|
||||||
|
FrameType ret = std::move(*ret_ptr);
|
||||||
|
|
||||||
|
_queue.pop();
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool push(const FrameType& value) override {
|
||||||
|
if (_lossy) {
|
||||||
|
[[maybe_unused]] auto _ = _queue.try_emplace(value);
|
||||||
|
// TODO: maybe return ?
|
||||||
|
} else {
|
||||||
|
_queue.push(value);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// implements a stream that pushes to all sub streams
|
||||||
|
// release all streams before destructing! // TODO: improve lifetime here, maybe some shared semaphore?
|
||||||
|
template<typename FrameType, typename SubStreamType = QueuedFrameStream2<FrameType>>
|
||||||
|
struct FrameStream2MultiSource : public FrameStream2SourceI<FrameType>, public FrameStream2I<FrameType> {
|
||||||
|
using sub_stream_type_t = SubStreamType;
|
||||||
|
|
||||||
|
// pointer stability
|
||||||
|
std::vector<std::shared_ptr<SubStreamType>> _sub_streams;
|
||||||
|
std::mutex _sub_stream_lock; // accessing the _sub_streams array needs to be exclusive
|
||||||
|
// a simple lock here is ok, since this tends to be a rare operation,
|
||||||
|
// except for the push, which is always on the same thread
|
||||||
|
|
||||||
|
virtual ~FrameStream2MultiSource(void) {}
|
||||||
|
|
||||||
|
// TODO: forward args instead
|
||||||
|
std::shared_ptr<FrameStream2I<FrameType>> subscribe(void) override {
|
||||||
|
// TODO: args???
|
||||||
|
size_t queue_size = 8;
|
||||||
|
bool lossy = true;
|
||||||
|
|
||||||
|
std::lock_guard lg{_sub_stream_lock};
|
||||||
|
return _sub_streams.emplace_back(std::make_unique<SubStreamType>(queue_size, lossy));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool unsubscribe(const std::shared_ptr<FrameStream2I<FrameType>>& sub) override {
|
||||||
|
std::lock_guard lg{_sub_stream_lock};
|
||||||
|
for (auto it = _sub_streams.begin(); it != _sub_streams.end(); it++) {
|
||||||
|
if (*it == sub) {
|
||||||
|
_sub_streams.erase(it);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false; // ?
|
||||||
|
}
|
||||||
|
|
||||||
|
// stream interface
|
||||||
|
|
||||||
|
int32_t size(void) override {
|
||||||
|
// TODO: return something sensible?
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<FrameType> pop(void) override {
|
||||||
|
// nope
|
||||||
|
assert(false && "this logic is very frame type specific, provide an impl");
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns true if there are readers
|
||||||
|
bool push(const FrameType& value) override {
|
||||||
|
std::lock_guard lg{_sub_stream_lock};
|
||||||
|
bool have_readers{false};
|
||||||
|
for (auto& it : _sub_streams) {
|
||||||
|
[[maybe_unused]] auto _ = it->push(value);
|
||||||
|
have_readers = true; // even if queue full, we still continue believing in them
|
||||||
|
// maybe consider push return value?
|
||||||
|
}
|
||||||
|
return have_readers;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
289
src/content/sdl_audio_frame_stream2.cpp
Normal file
289
src/content/sdl_audio_frame_stream2.cpp
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
#include "./sdl_audio_frame_stream2.hpp"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
// "thin" wrapper around sdl audio streams
|
||||||
|
// we dont needs to get fance, as they already provide everything we need
|
||||||
|
struct SDLAudioStreamReader : public AudioFrameStream2I {
|
||||||
|
std::unique_ptr<SDL_AudioStream, decltype(&SDL_DestroyAudioStream)> _stream;
|
||||||
|
|
||||||
|
uint32_t _seq_counter {0};
|
||||||
|
|
||||||
|
uint32_t _sample_rate {48'000};
|
||||||
|
size_t _channels {0};
|
||||||
|
SDL_AudioFormat _format {SDL_AUDIO_S16};
|
||||||
|
|
||||||
|
std::vector<int16_t> _buffer;
|
||||||
|
|
||||||
|
SDLAudioStreamReader(void) : _stream(nullptr, nullptr) {}
|
||||||
|
SDLAudioStreamReader(SDLAudioStreamReader&& other) :
|
||||||
|
_stream(std::move(other._stream)),
|
||||||
|
_sample_rate(other._sample_rate),
|
||||||
|
_channels(other._channels),
|
||||||
|
_format(other._format)
|
||||||
|
{
|
||||||
|
static const size_t buffer_size {960*_channels};
|
||||||
|
_buffer.resize(buffer_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
~SDLAudioStreamReader(void) {
|
||||||
|
if (_stream) {
|
||||||
|
SDL_UnbindAudioStream(_stream.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t size(void) override {
|
||||||
|
//assert(_stream);
|
||||||
|
// returns bytes
|
||||||
|
//SDL_GetAudioStreamAvailable(_stream.get());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<AudioFrame> pop(void) override {
|
||||||
|
assert(_stream);
|
||||||
|
assert(_format == SDL_AUDIO_S16);
|
||||||
|
|
||||||
|
static const size_t buffer_size {960*_channels};
|
||||||
|
_buffer.resize(buffer_size); // noop?
|
||||||
|
|
||||||
|
const auto read_bytes = SDL_GetAudioStreamData(
|
||||||
|
_stream.get(),
|
||||||
|
_buffer.data(),
|
||||||
|
_buffer.size()*sizeof(int16_t)
|
||||||
|
);
|
||||||
|
|
||||||
|
// no new frame yet, or error
|
||||||
|
if (read_bytes <= 0) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AudioFrame {
|
||||||
|
_seq_counter++,
|
||||||
|
_sample_rate, _channels,
|
||||||
|
Span<int16_t>(_buffer.data(), read_bytes/sizeof(int16_t)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool push(const AudioFrame&) override {
|
||||||
|
// TODO: make universal sdl stream wrapper (combine with SDLAudioOutputDeviceDefaultInstance)
|
||||||
|
assert(false && "read only");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
SDLAudioInputDevice::SDLAudioInputDevice(void) : SDLAudioInputDevice(SDL_AUDIO_DEVICE_DEFAULT_RECORDING) {
|
||||||
|
}
|
||||||
|
|
||||||
|
SDLAudioInputDevice::SDLAudioInputDevice(SDL_AudioDeviceID conf_device_id) : _configured_device_id(conf_device_id) {
|
||||||
|
if (_configured_device_id == 0) {
|
||||||
|
// TODO: proper error handling
|
||||||
|
throw int(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDLAudioInputDevice::~SDLAudioInputDevice(void) {
|
||||||
|
_streams.clear();
|
||||||
|
|
||||||
|
if (_virtual_device_id != 0) {
|
||||||
|
SDL_CloseAudioDevice(_virtual_device_id);
|
||||||
|
_virtual_device_id = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<FrameStream2I<AudioFrame>> SDLAudioInputDevice::subscribe(void) {
|
||||||
|
if (_virtual_device_id == 0) {
|
||||||
|
// first stream, open device
|
||||||
|
// this spec is more like a hint to the hardware
|
||||||
|
SDL_AudioSpec spec {
|
||||||
|
SDL_AUDIO_S16,
|
||||||
|
1, // TODO: conf
|
||||||
|
48'000,
|
||||||
|
};
|
||||||
|
_virtual_device_id = SDL_OpenAudioDevice(_configured_device_id, &spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_virtual_device_id == 0) {
|
||||||
|
std::cerr << "SDLAID error: failed opening device " << _configured_device_id << "\n";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_AudioSpec spec {
|
||||||
|
SDL_AUDIO_S16,
|
||||||
|
1, // TODO: conf
|
||||||
|
48'000,
|
||||||
|
};
|
||||||
|
|
||||||
|
SDL_AudioSpec device_spec {
|
||||||
|
SDL_AUDIO_S16,
|
||||||
|
1, // TODO: conf
|
||||||
|
48'000,
|
||||||
|
};
|
||||||
|
// TODO: error check
|
||||||
|
SDL_GetAudioDeviceFormat(_virtual_device_id, &device_spec, nullptr);
|
||||||
|
|
||||||
|
// error check
|
||||||
|
auto* sdl_stream = SDL_CreateAudioStream(&device_spec, &spec);
|
||||||
|
|
||||||
|
// error check
|
||||||
|
SDL_BindAudioStream(_virtual_device_id, sdl_stream);
|
||||||
|
|
||||||
|
auto new_stream = std::make_shared<SDLAudioStreamReader>();
|
||||||
|
// TODO: move to ctr
|
||||||
|
new_stream->_stream = {sdl_stream, &SDL_DestroyAudioStream};
|
||||||
|
new_stream->_sample_rate = spec.freq;
|
||||||
|
new_stream->_channels = spec.channels;
|
||||||
|
new_stream->_format = spec.format;
|
||||||
|
|
||||||
|
_streams.emplace_back(new_stream);
|
||||||
|
|
||||||
|
return new_stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SDLAudioInputDevice::unsubscribe(const std::shared_ptr<FrameStream2I<AudioFrame>>& sub) {
|
||||||
|
for (auto it = _streams.cbegin(); it != _streams.cend(); it++) {
|
||||||
|
if (*it == sub) {
|
||||||
|
_streams.erase(it);
|
||||||
|
if (_streams.empty()) {
|
||||||
|
// last stream, close
|
||||||
|
// TODO: make sure no shared ptr still exists???
|
||||||
|
SDL_CloseAudioDevice(_virtual_device_id);
|
||||||
|
std::cout << "SDLAID: closing device " << _virtual_device_id << " (" << _configured_device_id << ")\n";
|
||||||
|
_virtual_device_id = 0;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// does not need to be visible in the header
|
||||||
|
struct SDLAudioOutputDeviceDefaultInstance : public AudioFrameStream2I {
|
||||||
|
std::unique_ptr<SDL_AudioStream, decltype(&SDL_DestroyAudioStream)> _stream;
|
||||||
|
|
||||||
|
uint32_t _last_sample_rate {48'000};
|
||||||
|
size_t _last_channels {0};
|
||||||
|
SDL_AudioFormat _last_format {SDL_AUDIO_S16};
|
||||||
|
|
||||||
|
// TODO: audio device
|
||||||
|
SDLAudioOutputDeviceDefaultInstance(void);
|
||||||
|
SDLAudioOutputDeviceDefaultInstance(SDLAudioOutputDeviceDefaultInstance&& other);
|
||||||
|
|
||||||
|
~SDLAudioOutputDeviceDefaultInstance(void);
|
||||||
|
|
||||||
|
int32_t size(void) override;
|
||||||
|
std::optional<AudioFrame> pop(void) override;
|
||||||
|
bool push(const AudioFrame& value) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
SDLAudioOutputDeviceDefaultInstance::SDLAudioOutputDeviceDefaultInstance(void) : _stream(nullptr, nullptr) {
|
||||||
|
}
|
||||||
|
|
||||||
|
SDLAudioOutputDeviceDefaultInstance::SDLAudioOutputDeviceDefaultInstance(SDLAudioOutputDeviceDefaultInstance&& other) : _stream(std::move(other._stream)) {
|
||||||
|
}
|
||||||
|
|
||||||
|
SDLAudioOutputDeviceDefaultInstance::~SDLAudioOutputDeviceDefaultInstance(void) {
|
||||||
|
}
|
||||||
|
int32_t SDLAudioOutputDeviceDefaultInstance::size(void) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<AudioFrame> SDLAudioOutputDeviceDefaultInstance::pop(void) {
|
||||||
|
assert(false);
|
||||||
|
// this is an output device, there is no data to pop
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SDLAudioOutputDeviceDefaultInstance::push(const AudioFrame& value) {
|
||||||
|
if (!static_cast<bool>(_stream)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify here the fame has the same sample type, channel count and sample freq
|
||||||
|
// if something changed, we need to either use a temporary stream, just for conversion, or reopen _stream with the new params
|
||||||
|
// because of data temporality, the second options looks like a better candidate
|
||||||
|
if (
|
||||||
|
value.sample_rate != _last_sample_rate ||
|
||||||
|
value.channels != _last_channels ||
|
||||||
|
(value.isF32() && _last_format != SDL_AUDIO_F32) ||
|
||||||
|
(value.isS16() && _last_format != SDL_AUDIO_S16)
|
||||||
|
) {
|
||||||
|
const SDL_AudioSpec spec = {
|
||||||
|
static_cast<SDL_AudioFormat>((value.isF32() ? SDL_AUDIO_F32 : SDL_AUDIO_S16)),
|
||||||
|
static_cast<int>(value.channels),
|
||||||
|
static_cast<int>(value.sample_rate)
|
||||||
|
};
|
||||||
|
|
||||||
|
SDL_SetAudioStreamFormat(_stream.get(), &spec, nullptr);
|
||||||
|
|
||||||
|
std::cerr << "SDLAOD: audio format changed\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.isS16()) {
|
||||||
|
auto data = value.getSpan<int16_t>();
|
||||||
|
|
||||||
|
if (data.size == 0) {
|
||||||
|
std::cerr << "empty audio frame??\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SDL_PutAudioStreamData(_stream.get(), data.ptr, data.size * sizeof(int16_t))) {
|
||||||
|
std::cerr << "put data error\n";
|
||||||
|
return false; // return true?
|
||||||
|
}
|
||||||
|
} else if (value.isF32()) {
|
||||||
|
auto data = value.getSpan<float>();
|
||||||
|
|
||||||
|
if (data.size == 0) {
|
||||||
|
std::cerr << "empty audio frame??\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SDL_PutAudioStreamData(_stream.get(), data.ptr, data.size * sizeof(float))) {
|
||||||
|
std::cerr << "put data error\n";
|
||||||
|
return false; // return true?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_last_sample_rate = value.sample_rate;
|
||||||
|
_last_channels = value.channels;
|
||||||
|
_last_format = value.isF32() ? SDL_AUDIO_F32 : SDL_AUDIO_S16;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SDLAudioOutputDeviceDefaultSink::~SDLAudioOutputDeviceDefaultSink(void) {
|
||||||
|
// TODO: pause and close device?
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<FrameStream2I<AudioFrame>> SDLAudioOutputDeviceDefaultSink::subscribe(void) {
|
||||||
|
auto new_instance = std::make_shared<SDLAudioOutputDeviceDefaultInstance>();
|
||||||
|
|
||||||
|
constexpr SDL_AudioSpec spec = { SDL_AUDIO_S16, 1, 48000 };
|
||||||
|
|
||||||
|
new_instance->_stream = {
|
||||||
|
SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr, nullptr),
|
||||||
|
&SDL_DestroyAudioStream
|
||||||
|
};
|
||||||
|
new_instance->_last_sample_rate = spec.freq;
|
||||||
|
new_instance->_last_channels = spec.channels;
|
||||||
|
new_instance->_last_format = spec.format;
|
||||||
|
|
||||||
|
if (!static_cast<bool>(new_instance->_stream)) {
|
||||||
|
std::cerr << "SDL open audio device failed!\n";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto audio_device_id = SDL_GetAudioStreamDevice(new_instance->_stream.get());
|
||||||
|
SDL_ResumeAudioDevice(audio_device_id);
|
||||||
|
|
||||||
|
return new_instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SDLAudioOutputDeviceDefaultSink::unsubscribe(const std::shared_ptr<FrameStream2I<AudioFrame>>& sub) {
|
||||||
|
if (!sub) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
43
src/content/sdl_audio_frame_stream2.hpp
Normal file
43
src/content/sdl_audio_frame_stream2.hpp
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "./frame_stream2.hpp"
|
||||||
|
#include "./audio_stream.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// we dont have to multicast ourself, because sdl streams and virtual devices already do this
|
||||||
|
|
||||||
|
// source
|
||||||
|
// opens device
|
||||||
|
// creates a sdl audio stream for each subscribed reader stream
|
||||||
|
struct SDLAudioInputDevice : public FrameStream2SourceI<AudioFrame> {
|
||||||
|
// held by instances
|
||||||
|
using sdl_stream_type = std::unique_ptr<SDL_AudioStream, decltype(&SDL_DestroyAudioStream)>;
|
||||||
|
|
||||||
|
SDL_AudioDeviceID _configured_device_id {0};
|
||||||
|
SDL_AudioDeviceID _virtual_device_id {0};
|
||||||
|
|
||||||
|
std::vector<std::shared_ptr<FrameStream2I<AudioFrame>>> _streams;
|
||||||
|
|
||||||
|
SDLAudioInputDevice(void);
|
||||||
|
SDLAudioInputDevice(SDL_AudioDeviceID conf_device_id);
|
||||||
|
~SDLAudioInputDevice(void);
|
||||||
|
|
||||||
|
std::shared_ptr<FrameStream2I<AudioFrame>> subscribe(void) override;
|
||||||
|
bool unsubscribe(const std::shared_ptr<FrameStream2I<AudioFrame>>& sub) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
// sink
|
||||||
|
// constructs entirely new streams, since sdl handles sync and mixing for us (or should)
|
||||||
|
struct SDLAudioOutputDeviceDefaultSink : public FrameStream2SinkI<AudioFrame> {
|
||||||
|
// TODO: pause device?
|
||||||
|
|
||||||
|
~SDLAudioOutputDeviceDefaultSink(void);
|
||||||
|
|
||||||
|
std::shared_ptr<FrameStream2I<AudioFrame>> subscribe(void) override;
|
||||||
|
bool unsubscribe(const std::shared_ptr<FrameStream2I<AudioFrame>>& sub) override;
|
||||||
|
};
|
||||||
|
|
167
src/content/sdl_video_frame_stream2.cpp
Normal file
167
src/content/sdl_video_frame_stream2.cpp
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
#include "./sdl_video_frame_stream2.hpp"
|
||||||
|
#include "SDL3/SDL_camera.h"
|
||||||
|
#include "SDL3/SDL_pixels.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iostream>
|
||||||
|
#include <memory>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
// TODO: move out and create lazy cam for each device
|
||||||
|
SDLVideoCameraContent::SDLVideoCameraContent(void) {
|
||||||
|
int devcount {0};
|
||||||
|
//SDL_CameraDeviceID *devices = SDL_GetCameraDevices(&devcount);
|
||||||
|
SDL_CameraID *devices = SDL_GetCameras(&devcount);
|
||||||
|
std::cout << "SDL Camera Driver: " << SDL_GetCurrentCameraDriver() << "\n";
|
||||||
|
|
||||||
|
if (devices == nullptr || devcount < 1) {
|
||||||
|
throw int(1); // TODO: proper exception?
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "### found cameras:\n";
|
||||||
|
for (int i = 0; i < devcount; i++) {
|
||||||
|
const SDL_CameraID device = devices[i];
|
||||||
|
|
||||||
|
const char *name = SDL_GetCameraName(device);
|
||||||
|
std::cout << " - Camera #" << i << ": " << name << "\n";
|
||||||
|
|
||||||
|
int speccount {0};
|
||||||
|
SDL_CameraSpec** specs = SDL_GetCameraSupportedFormats(device, &speccount);
|
||||||
|
if (specs == nullptr) {
|
||||||
|
std::cout << " - no supported spec\n";
|
||||||
|
} else {
|
||||||
|
for (int spec_i = 0; spec_i < speccount; spec_i++) {
|
||||||
|
std::cout << " - " << specs[spec_i]->width << "x" << specs[spec_i]->height << "@" << float(specs[spec_i]->framerate_numerator)/specs[spec_i]->framerate_denominator << "fps " << SDL_GetPixelFormatName(specs[spec_i]->format) << "\n";
|
||||||
|
|
||||||
|
}
|
||||||
|
SDL_free(specs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
SDL_CameraSpec spec {
|
||||||
|
// FORCE a diffrent pixel format
|
||||||
|
//SDL_PIXELFORMAT_RGBA8888,
|
||||||
|
//SDL_PIXELFORMAT_UNKNOWN,
|
||||||
|
//SDL_PIXELFORMAT_IYUV,
|
||||||
|
SDL_PIXELFORMAT_YUY2,
|
||||||
|
|
||||||
|
//SDL_COLORSPACE_UNKNOWN,
|
||||||
|
//SDL_COLORSPACE_SRGB,
|
||||||
|
//SDL_COLORSPACE_SRGB_LINEAR,
|
||||||
|
SDL_COLORSPACE_YUV_DEFAULT,
|
||||||
|
|
||||||
|
//1280, 720,
|
||||||
|
//640, 360,
|
||||||
|
//640, 480,
|
||||||
|
696, 392,
|
||||||
|
|
||||||
|
//1, 30
|
||||||
|
30, 1
|
||||||
|
};
|
||||||
|
_camera = {
|
||||||
|
//SDL_OpenCamera(devices[devcount-1], &spec),
|
||||||
|
SDL_OpenCamera(devices[0], nullptr),
|
||||||
|
//SDL_OpenCamera(devices[0], &spec),
|
||||||
|
&SDL_CloseCamera
|
||||||
|
};
|
||||||
|
SDL_GetCameraFormat(_camera.get(), &spec);
|
||||||
|
}
|
||||||
|
SDL_free(devices);
|
||||||
|
if (!static_cast<bool>(_camera)) {
|
||||||
|
throw int(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (SDL_GetCameraPermissionState(_camera.get()) == 0) {
|
||||||
|
std::cerr << "permission for camera not granted\n";
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SDL_GetCameraPermissionState(_camera.get()) < 0) {
|
||||||
|
std::cerr << "user denied camera permission\n";
|
||||||
|
throw int(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_CameraSpec spec;
|
||||||
|
float fps {1.f};
|
||||||
|
if (!SDL_GetCameraFormat(_camera.get(), &spec)) {
|
||||||
|
// meh
|
||||||
|
throw int(5);
|
||||||
|
} else {
|
||||||
|
fps = float(spec.framerate_numerator)/float(spec.framerate_denominator);
|
||||||
|
std::cout << "camera fps: " << fps << "fps (" << spec.framerate_numerator << "/" << spec.framerate_denominator << ")\n";
|
||||||
|
auto* format_name = SDL_GetPixelFormatName(spec.format);
|
||||||
|
std::cout << "camera format: " << format_name << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
_thread = std::thread([this, fps](void) {
|
||||||
|
while (!_thread_should_quit) {
|
||||||
|
Uint64 timestampNS = 0;
|
||||||
|
SDL_Surface* sdl_frame_next = SDL_AcquireCameraFrame(_camera.get(), ×tampNS);
|
||||||
|
|
||||||
|
// no new frame yet, or error
|
||||||
|
if (sdl_frame_next == nullptr) {
|
||||||
|
// only sleep 1/10, we expected a frame
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(int64_t((1000/fps) / 10)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
{ // test copy to trigger bug
|
||||||
|
SDL_Surface* test_surf = SDL_CreateSurface(
|
||||||
|
sdl_frame_next->w,
|
||||||
|
sdl_frame_next->h,
|
||||||
|
SDL_PIXELFORMAT_RGBA8888
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(test_surf != nullptr);
|
||||||
|
|
||||||
|
SDL_BlitSurface(sdl_frame_next, nullptr, test_surf, nullptr);
|
||||||
|
|
||||||
|
SDL_DestroySurface(test_surf);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool someone_listening {false};
|
||||||
|
{
|
||||||
|
SDLVideoFrame new_frame_non_owning {
|
||||||
|
timestampNS/1000,
|
||||||
|
sdl_frame_next
|
||||||
|
};
|
||||||
|
|
||||||
|
// creates surface copies
|
||||||
|
someone_listening = push(new_frame_non_owning);
|
||||||
|
}
|
||||||
|
SDL_ReleaseCameraFrame(_camera.get(), sdl_frame_next);
|
||||||
|
|
||||||
|
if (someone_listening) {
|
||||||
|
// double the interval on acquire
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(int64_t((1000/fps)*0.5)));
|
||||||
|
} else {
|
||||||
|
std::cerr << "i guess no one is listening\n";
|
||||||
|
// we just sleep 4x as long, bc no one is listening
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(int64_t((1000/fps)*4)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SDLVideoCameraContent::~SDLVideoCameraContent(void) {
|
||||||
|
_thread_should_quit = true;
|
||||||
|
_thread.join();
|
||||||
|
// TODO: what to do if readers are still present?
|
||||||
|
|
||||||
|
// HACK: sdl is buggy and freezes otherwise. it is likely still possible, but rare to freeze here
|
||||||
|
// flush unused frames
|
||||||
|
#if 1
|
||||||
|
while (true) {
|
||||||
|
SDL_Surface* sdl_frame_next = SDL_AcquireCameraFrame(_camera.get(), nullptr);
|
||||||
|
if (sdl_frame_next != nullptr) {
|
||||||
|
SDL_ReleaseCameraFrame(_camera.get(), sdl_frame_next);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
71
src/content/sdl_video_frame_stream2.hpp
Normal file
71
src/content/sdl_video_frame_stream2.hpp
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "./frame_stream2.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
inline void nopSurfaceDestructor(SDL_Surface*) {}
|
||||||
|
|
||||||
|
// this is very sdl specific
|
||||||
|
struct SDLVideoFrame {
|
||||||
|
// TODO: sequence numbering?
|
||||||
|
// micro seconds (nano is way too much)
|
||||||
|
uint64_t timestampUS {0};
|
||||||
|
|
||||||
|
std::unique_ptr<SDL_Surface, decltype(&SDL_DestroySurface)> surface {nullptr, &SDL_DestroySurface};
|
||||||
|
|
||||||
|
// special non-owning constructor?
|
||||||
|
SDLVideoFrame(
|
||||||
|
uint64_t ts,
|
||||||
|
SDL_Surface* surf
|
||||||
|
) {
|
||||||
|
timestampUS = ts;
|
||||||
|
surface = {surf, &nopSurfaceDestructor};
|
||||||
|
}
|
||||||
|
SDLVideoFrame(SDLVideoFrame&& other) = default;
|
||||||
|
// copy
|
||||||
|
SDLVideoFrame(const SDLVideoFrame& other) {
|
||||||
|
timestampUS = other.timestampUS;
|
||||||
|
if (static_cast<bool>(other.surface)) {
|
||||||
|
//surface = {
|
||||||
|
// SDL_CreateSurface(
|
||||||
|
// other.surface->w,
|
||||||
|
// other.surface->h,
|
||||||
|
// other.surface->format
|
||||||
|
// ),
|
||||||
|
// &SDL_DestroySurface
|
||||||
|
//};
|
||||||
|
//SDL_BlitSurface(other.surface.get(), nullptr, surface.get(), nullptr);
|
||||||
|
surface = {
|
||||||
|
SDL_DuplicateSurface(other.surface.get()),
|
||||||
|
&SDL_DestroySurface
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SDLVideoFrame& operator=(const SDLVideoFrame& other) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
using SDLVideoFrameStream2MultiSource = FrameStream2MultiSource<SDLVideoFrame>;
|
||||||
|
using SDLVideoFrameStream2 = SDLVideoFrameStream2MultiSource::sub_stream_type_t; // just use the default for now
|
||||||
|
|
||||||
|
struct SDLVideoCameraContent : public SDLVideoFrameStream2MultiSource {
|
||||||
|
// meh, empty default
|
||||||
|
std::unique_ptr<SDL_Camera, decltype(&SDL_CloseCamera)> _camera {nullptr, &SDL_CloseCamera};
|
||||||
|
std::atomic<bool> _thread_should_quit {false};
|
||||||
|
std::thread _thread;
|
||||||
|
|
||||||
|
// construct source and start thread
|
||||||
|
// TODO: optimize so the thread is not always running
|
||||||
|
SDLVideoCameraContent(void);
|
||||||
|
|
||||||
|
// stops the thread and closes the camera
|
||||||
|
~SDLVideoCameraContent(void);
|
||||||
|
|
||||||
|
// make only some of writer public
|
||||||
|
using SDLVideoFrameStream2MultiSource::subscribe;
|
||||||
|
using SDLVideoFrameStream2MultiSource::unsubscribe;
|
||||||
|
};
|
||||||
|
|
15
src/content/stream_reader.hpp
Normal file
15
src/content/stream_reader.hpp
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <solanaceae/util/span.hpp>
|
||||||
|
|
||||||
|
// most media that can be counted as "stream" comes in packets/frames/messages
|
||||||
|
// so this class provides an interface for ideal async fetching of frames
|
||||||
|
struct RawFrameStreamReaderI {
|
||||||
|
// return the number of ready frames in cache
|
||||||
|
// returns -1 if unknown
|
||||||
|
virtual int64_t have(void) = 0;
|
||||||
|
|
||||||
|
// get next frame, empty if none
|
||||||
|
virtual ByteSpan getNext(void) = 0;
|
||||||
|
};
|
||||||
|
|
39
src/content/stream_reader_sdl_audio.cpp
Normal file
39
src/content/stream_reader_sdl_audio.cpp
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#include "./stream_reader_sdl_audio.hpp"
|
||||||
|
|
||||||
|
SDLAudioFrameStreamReader::SDLAudioFrameStreamReader(int32_t buffer_size) : _buffer_size(buffer_size), _stream{nullptr, &SDL_DestroyAudioStream} {
|
||||||
|
_buffer.resize(_buffer_size);
|
||||||
|
|
||||||
|
const SDL_AudioSpec spec = { SDL_AUDIO_S16, 1, 48000 };
|
||||||
|
|
||||||
|
_stream = {
|
||||||
|
SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_CAPTURE, &spec, nullptr, nullptr),
|
||||||
|
&SDL_DestroyAudioStream
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<int16_t> SDLAudioFrameStreamReader::getNextAudioFrame(void) {
|
||||||
|
const int32_t needed_bytes = (_buffer.size() - _remaining_size) * sizeof(int16_t);
|
||||||
|
const auto read_bytes = SDL_GetAudioStreamData(_stream.get(), _buffer.data()+_remaining_size, needed_bytes);
|
||||||
|
if (read_bytes < 0) {
|
||||||
|
// error
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (read_bytes < needed_bytes) {
|
||||||
|
// HACK: we are just assuming here that sdl never gives us half a sample!
|
||||||
|
_remaining_size += read_bytes / sizeof(int16_t);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
_remaining_size = 0;
|
||||||
|
return Span<int16_t>{_buffer};
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t SDLAudioFrameStreamReader::have(void) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteSpan SDLAudioFrameStreamReader::getNext(void) {
|
||||||
|
auto next_frame_span = getNextAudioFrame();
|
||||||
|
return ByteSpan{reinterpret_cast<const uint8_t*>(next_frame_span.ptr), next_frame_span.size};
|
||||||
|
}
|
||||||
|
|
31
src/content/stream_reader_sdl_audio.hpp
Normal file
31
src/content/stream_reader_sdl_audio.hpp
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "./stream_reader.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
struct SDLAudioFrameStreamReader : public RawFrameStreamReaderI {
|
||||||
|
// count samples per buffer
|
||||||
|
const int32_t _buffer_size {1024};
|
||||||
|
std::vector<int16_t> _buffer;
|
||||||
|
size_t _remaining_size {0}; // data still in buffer, that was remaining from last call and not enough to fill a full frame
|
||||||
|
|
||||||
|
// meh, empty default
|
||||||
|
std::unique_ptr<SDL_AudioStream, decltype(&SDL_DestroyAudioStream)> _stream;
|
||||||
|
|
||||||
|
// buffer_size in number of samples
|
||||||
|
SDLAudioFrameStreamReader(int32_t buffer_size = 1024);
|
||||||
|
|
||||||
|
// data owned by StreamReader, overwritten by next call to getNext*()
|
||||||
|
Span<int16_t> getNextAudioFrame(void);
|
||||||
|
|
||||||
|
public: // interface
|
||||||
|
int64_t have(void) override;
|
||||||
|
|
||||||
|
ByteSpan getNext(void) override;
|
||||||
|
};
|
||||||
|
|
84
src/content/stream_reader_sdl_video.cpp
Normal file
84
src/content/stream_reader_sdl_video.cpp
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
#include "./stream_reader_sdl_video.hpp"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
SDLVideoFrameStreamReader::SDLVideoFrameStreamReader() : _camera{nullptr, &SDL_CloseCamera}, _surface{nullptr, &SDL_DestroySurface} {
|
||||||
|
// enumerate
|
||||||
|
int devcount = 0;
|
||||||
|
SDL_CameraDeviceID *devices = SDL_GetCameraDevices(&devcount);
|
||||||
|
|
||||||
|
if (devices == nullptr || devcount < 1) {
|
||||||
|
throw int(1); // TODO: proper exception?
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "### found cameras:\n";
|
||||||
|
for (int i = 0; i < devcount; i++) {
|
||||||
|
const SDL_CameraDeviceID device = devices[i];
|
||||||
|
char *name = SDL_GetCameraDeviceName(device);
|
||||||
|
|
||||||
|
std::cout << " - Camera #" << i << ": " << name << "\n";
|
||||||
|
|
||||||
|
SDL_free(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
_camera = {
|
||||||
|
SDL_OpenCameraDevice(devices[0], nullptr),
|
||||||
|
&SDL_CloseCamera
|
||||||
|
};
|
||||||
|
if (!static_cast<bool>(_camera)) {
|
||||||
|
throw int(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_CameraSpec spec;
|
||||||
|
if (SDL_GetCameraFormat(_camera.get(), &spec) < 0) {
|
||||||
|
// meh
|
||||||
|
} else {
|
||||||
|
// interval
|
||||||
|
float interval = float(spec.interval_numerator)/float(spec.interval_denominator);
|
||||||
|
std::cout << "camera interval: " << interval*1000 << "ms\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDLVideoFrameStreamReader::VideoFrame SDLVideoFrameStreamReader::getNextVideoFrameRGBA(void) {
|
||||||
|
if (!static_cast<bool>(_camera)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint64 timestampNS = 0;
|
||||||
|
SDL_Surface* frame_next = SDL_AcquireCameraFrame(_camera.get(), ×tampNS);
|
||||||
|
|
||||||
|
// no new frame yet, or error
|
||||||
|
if (frame_next == nullptr) {
|
||||||
|
//std::cout << "failed acquiring frame\n";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: investigate zero copy
|
||||||
|
_surface = {
|
||||||
|
SDL_ConvertSurfaceFormat(frame_next, SDL_PIXELFORMAT_RGBA8888),
|
||||||
|
&SDL_DestroySurface
|
||||||
|
};
|
||||||
|
|
||||||
|
SDL_ReleaseCameraFrame(_camera.get(), frame_next);
|
||||||
|
|
||||||
|
SDL_LockSurface(_surface.get());
|
||||||
|
|
||||||
|
return {
|
||||||
|
_surface->w,
|
||||||
|
_surface->h,
|
||||||
|
timestampNS,
|
||||||
|
{
|
||||||
|
reinterpret_cast<const uint8_t*>(_surface->pixels),
|
||||||
|
uint64_t(_surface->w*_surface->h*4) // rgba
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
int64_t SDLVideoFrameStreamReader::have(void) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteSpan SDLVideoFrameStreamReader::getNext(void) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
34
src/content/stream_reader_sdl_video.hpp
Normal file
34
src/content/stream_reader_sdl_video.hpp
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "./stream_reader.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
struct SDLVideoFrameStreamReader : public RawFrameStreamReaderI {
|
||||||
|
// meh, empty default
|
||||||
|
std::unique_ptr<SDL_Camera, decltype(&SDL_CloseCamera)> _camera;
|
||||||
|
std::unique_ptr<SDL_Surface, decltype(&SDL_DestroySurface)> _surface;
|
||||||
|
|
||||||
|
SDLVideoFrameStreamReader(void);
|
||||||
|
|
||||||
|
struct VideoFrame {
|
||||||
|
int32_t width {0};
|
||||||
|
int32_t height {0};
|
||||||
|
|
||||||
|
uint64_t timestampNS {0};
|
||||||
|
|
||||||
|
ByteSpan data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// data owned by StreamReader, overwritten by next call to getNext*()
|
||||||
|
VideoFrame getNextVideoFrameRGBA(void);
|
||||||
|
|
||||||
|
public: // interface
|
||||||
|
int64_t have(void) override;
|
||||||
|
ByteSpan getNext(void) override;
|
||||||
|
};
|
||||||
|
|
718
src/debug_tox_call.cpp
Normal file
718
src/debug_tox_call.cpp
Normal file
@ -0,0 +1,718 @@
|
|||||||
|
#include "./debug_tox_call.hpp"
|
||||||
|
|
||||||
|
#include "./stream_manager.hpp"
|
||||||
|
#include "./content/audio_stream.hpp"
|
||||||
|
#include "./content/sdl_video_frame_stream2.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <imgui/imgui.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
// fwd
|
||||||
|
namespace Message {
|
||||||
|
uint64_t getTimeMS();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool isFormatPlanar(SDL_PixelFormat f) {
|
||||||
|
return
|
||||||
|
f == SDL_PIXELFORMAT_YV12 ||
|
||||||
|
f == SDL_PIXELFORMAT_IYUV ||
|
||||||
|
f == SDL_PIXELFORMAT_YUY2 ||
|
||||||
|
f == SDL_PIXELFORMAT_UYVY ||
|
||||||
|
f == SDL_PIXELFORMAT_YVYU ||
|
||||||
|
f == SDL_PIXELFORMAT_NV12 ||
|
||||||
|
f == SDL_PIXELFORMAT_NV21 ||
|
||||||
|
f == SDL_PIXELFORMAT_P010
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SDL_Surface* convertYUY2_IYUV(SDL_Surface* surf) {
|
||||||
|
if (surf->format != SDL_PIXELFORMAT_YUY2) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if ((surf->w % 2) != 0) {
|
||||||
|
SDL_SetError("YUY2->IYUV does not support odd widths");
|
||||||
|
// hmmm, we dont handle odd widths
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LockSurface(surf);
|
||||||
|
|
||||||
|
SDL_Surface* conv_surf = SDL_CreateSurface(surf->w, surf->h, SDL_PIXELFORMAT_IYUV);
|
||||||
|
SDL_LockSurface(conv_surf);
|
||||||
|
|
||||||
|
// YUY2 is 4:2:2 packed
|
||||||
|
// Y is simple, we just copy it over
|
||||||
|
// U V are double the resolution (vertically), so we avg both
|
||||||
|
// Packed mode: Y0+U0+Y1+V0 (1 plane)
|
||||||
|
|
||||||
|
uint8_t* y_plane = static_cast<uint8_t*>(conv_surf->pixels);
|
||||||
|
uint8_t* u_plane = static_cast<uint8_t*>(conv_surf->pixels) + conv_surf->w*conv_surf->h;
|
||||||
|
uint8_t* v_plane = static_cast<uint8_t*>(conv_surf->pixels) + conv_surf->w*conv_surf->h + (conv_surf->w/2)*(conv_surf->h/2);
|
||||||
|
|
||||||
|
const uint8_t* yuy2_data = static_cast<const uint8_t*>(surf->pixels);
|
||||||
|
|
||||||
|
for (int y = 0; y < surf->h; y++) {
|
||||||
|
for (int x = 0; x < surf->w; x += 2) {
|
||||||
|
// every pixel uses 2 bytes
|
||||||
|
const uint8_t* yuy2_curser = yuy2_data + y*surf->w*2 + x*2;
|
||||||
|
uint8_t src_y0 = yuy2_curser[0];
|
||||||
|
uint8_t src_u = yuy2_curser[1];
|
||||||
|
uint8_t src_y1 = yuy2_curser[2];
|
||||||
|
uint8_t src_v = yuy2_curser[3];
|
||||||
|
|
||||||
|
y_plane[y*conv_surf->w + x] = src_y0;
|
||||||
|
y_plane[y*conv_surf->w + x+1] = src_y1;
|
||||||
|
|
||||||
|
size_t uv_index = (y/2) * (conv_surf->w/2) + x/2;
|
||||||
|
if (y % 2 == 0) {
|
||||||
|
// first write
|
||||||
|
u_plane[uv_index] = src_u;
|
||||||
|
v_plane[uv_index] = src_v;
|
||||||
|
} else {
|
||||||
|
// second write, mix with existing value
|
||||||
|
u_plane[uv_index] = (int(u_plane[uv_index]) + int(src_u)) / 2;
|
||||||
|
v_plane[uv_index] = (int(v_plane[uv_index]) + int(src_v)) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_UnlockSurface(conv_surf);
|
||||||
|
SDL_UnlockSurface(surf);
|
||||||
|
return conv_surf;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PushConversionQueuedVideoStream : public QueuedFrameStream2<SDLVideoFrame> {
|
||||||
|
SDL_PixelFormat _forced_format {SDL_PIXELFORMAT_IYUV};
|
||||||
|
|
||||||
|
PushConversionQueuedVideoStream(size_t queue_size, bool lossy = true) : QueuedFrameStream2<SDLVideoFrame>(queue_size, lossy) {}
|
||||||
|
~PushConversionQueuedVideoStream(void) {}
|
||||||
|
|
||||||
|
bool push(const SDLVideoFrame& value) override {
|
||||||
|
SDL_Surface* converted_surf = value.surface.get();
|
||||||
|
if (converted_surf->format != _forced_format) {
|
||||||
|
//std::cerr << "DTC: need to convert from " << SDL_GetPixelFormatName(converted_surf->format) << " to SDL_PIXELFORMAT_IYUV\n";
|
||||||
|
if (converted_surf->format == SDL_PIXELFORMAT_YUY2 && _forced_format == SDL_PIXELFORMAT_IYUV) {
|
||||||
|
// optimized custom impl
|
||||||
|
|
||||||
|
//auto start = Message::getTimeMS();
|
||||||
|
converted_surf = convertYUY2_IYUV(converted_surf);
|
||||||
|
//auto end = Message::getTimeMS();
|
||||||
|
// 3ms
|
||||||
|
//std::cerr << "DTC: timing " << SDL_GetPixelFormatName(converted_surf->format) << "->SDL_PIXELFORMAT_IYUV: " << end-start << "ms\n";
|
||||||
|
} else if (isFormatPlanar(converted_surf->format)) {
|
||||||
|
// meh, need to convert to rgb as a stopgap
|
||||||
|
|
||||||
|
//auto start = Message::getTimeMS();
|
||||||
|
//SDL_Surface* tmp_conv_surf = SDL_ConvertSurfaceAndColorspace(converted_surf, SDL_PIXELFORMAT_RGBA32, nullptr, SDL_COLORSPACE_RGB_DEFAULT, 0);
|
||||||
|
SDL_Surface* tmp_conv_surf = SDL_ConvertSurfaceAndColorspace(converted_surf, SDL_PIXELFORMAT_RGB24, nullptr, SDL_COLORSPACE_RGB_DEFAULT, 0);
|
||||||
|
//auto end = Message::getTimeMS();
|
||||||
|
// 1ms
|
||||||
|
//std::cerr << "DTC: timing " << SDL_GetPixelFormatName(converted_surf->format) << "->SDL_PIXELFORMAT_RGB24: " << end-start << "ms\n";
|
||||||
|
|
||||||
|
// TODO: fix sdl rgb->yuv conversion resulting in too dark (colorspace) issues
|
||||||
|
//start = Message::getTimeMS();
|
||||||
|
converted_surf = SDL_ConvertSurfaceAndColorspace(tmp_conv_surf, SDL_PIXELFORMAT_IYUV, nullptr, SDL_COLORSPACE_YUV_DEFAULT, 0);
|
||||||
|
//end = Message::getTimeMS();
|
||||||
|
// 60ms
|
||||||
|
//std::cerr << "DTC: timing SDL_PIXELFORMAT_RGB24->SDL_PIXELFORMAT_IYUV: " << end-start << "ms\n";
|
||||||
|
|
||||||
|
SDL_DestroySurface(tmp_conv_surf);
|
||||||
|
} else {
|
||||||
|
converted_surf = SDL_ConvertSurface(converted_surf, SDL_PIXELFORMAT_IYUV);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (converted_surf == nullptr) {
|
||||||
|
// oh god
|
||||||
|
std::cerr << "DTC error: failed to convert surface to IYUV: " << SDL_GetError() << "\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert(converted_surf != nullptr);
|
||||||
|
if (converted_surf != value.surface.get()) {
|
||||||
|
// TODO: add ctr with uptr
|
||||||
|
SDLVideoFrame new_value{value.timestampUS, nullptr};
|
||||||
|
new_value.surface = {
|
||||||
|
converted_surf,
|
||||||
|
&SDL_DestroySurface
|
||||||
|
};
|
||||||
|
|
||||||
|
return QueuedFrameStream2<SDLVideoFrame>::push(std::move(new_value));
|
||||||
|
} else {
|
||||||
|
return QueuedFrameStream2<SDLVideoFrame>::push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// exlusive
|
||||||
|
// TODO: replace with something better than a queue
|
||||||
|
struct ToxAVCallVideoSink : public FrameStream2SinkI<SDLVideoFrame> {
|
||||||
|
ToxAV& _toxav;
|
||||||
|
|
||||||
|
// bitrate for enabled state
|
||||||
|
uint32_t _video_bitrate {2};
|
||||||
|
|
||||||
|
uint32_t _fid;
|
||||||
|
std::shared_ptr<PushConversionQueuedVideoStream> _writer;
|
||||||
|
|
||||||
|
ToxAVCallVideoSink(ToxAV& toxav, uint32_t fid) : _toxav(toxav), _fid(fid) {}
|
||||||
|
~ToxAVCallVideoSink(void) {
|
||||||
|
if (_writer) {
|
||||||
|
_writer = nullptr;
|
||||||
|
_toxav.toxavVideoSetBitRate(_fid, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sink
|
||||||
|
std::shared_ptr<FrameStream2I<SDLVideoFrame>> subscribe(void) override {
|
||||||
|
if (_writer) {
|
||||||
|
// max 1 (exclusive, composite video somewhere else)
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto err = _toxav.toxavVideoSetBitRate(_fid, _video_bitrate);
|
||||||
|
if (err != TOXAV_ERR_BIT_RATE_SET_OK) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
_writer = std::make_shared<PushConversionQueuedVideoStream>(10, true);
|
||||||
|
|
||||||
|
return _writer;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool unsubscribe(const std::shared_ptr<FrameStream2I<SDLVideoFrame>>& sub) override {
|
||||||
|
if (!sub || !_writer) {
|
||||||
|
// nah
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub == _writer) {
|
||||||
|
_writer = nullptr;
|
||||||
|
|
||||||
|
/*auto err = */_toxav.toxavVideoSetBitRate(_fid, 0);
|
||||||
|
// print warning? on error?
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// what
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: make proper adapter
|
||||||
|
struct AudioStreamReFramer {
|
||||||
|
FrameStream2I<AudioFrame>* _stream {nullptr};
|
||||||
|
uint32_t frame_length_ms {10};
|
||||||
|
|
||||||
|
uint32_t own_seq_counter {0};
|
||||||
|
|
||||||
|
std::vector<int16_t> buffer;
|
||||||
|
size_t samples_in_buffer {0}; // absolute, so divide by ch for actual length
|
||||||
|
|
||||||
|
uint32_t seq {0};
|
||||||
|
uint32_t sample_rate {48'000};
|
||||||
|
size_t channels {0};
|
||||||
|
|
||||||
|
|
||||||
|
std::optional<AudioFrame> pop(void) {
|
||||||
|
assert(_stream != nullptr);
|
||||||
|
|
||||||
|
auto new_in = _stream->pop();
|
||||||
|
if (new_in.has_value()) {
|
||||||
|
auto& new_value = new_in.value();
|
||||||
|
assert(new_value.isS16());
|
||||||
|
if (!new_value.isS16()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(buffer.empty()) || // buffer not yet inited
|
||||||
|
(sample_rate != new_value.sample_rate || channels != new_value.channels) // not the same
|
||||||
|
) {
|
||||||
|
seq = 0;
|
||||||
|
sample_rate = new_value.sample_rate;
|
||||||
|
channels = new_value.channels;
|
||||||
|
|
||||||
|
// buffer does not exist or config changed and we discard
|
||||||
|
// preallocate to 2x desired buffer size
|
||||||
|
buffer = std::vector<int16_t>(2 * (channels*sample_rate*frame_length_ms)/1000);
|
||||||
|
samples_in_buffer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this will be very important in the future
|
||||||
|
// replace seq with timestapsUS like video??
|
||||||
|
#if 0
|
||||||
|
// some time / seq comparison shit
|
||||||
|
if (seq != 0 && new_value.seq != 0) {
|
||||||
|
if (seq+1 != new_value.seq) {
|
||||||
|
// we skipped shit
|
||||||
|
// TODO: insert silence to pad?
|
||||||
|
|
||||||
|
// drop existing
|
||||||
|
samples_in_buffer = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// update to latest
|
||||||
|
seq = new_value.seq;
|
||||||
|
|
||||||
|
|
||||||
|
auto new_span = new_value.getSpan<int16_t>();
|
||||||
|
|
||||||
|
//std::cout << "new incoming frame is " << new_value.getSpan<int16_t>().size/new_value.channels*1000/new_value.sample_rate << "ms\n";
|
||||||
|
|
||||||
|
// now append
|
||||||
|
// buffer too small
|
||||||
|
if (buffer.size() - samples_in_buffer < new_value.getSpan<int16_t>().size) {
|
||||||
|
buffer.resize(buffer.size() + new_value.getSpan<int16_t>().size - (buffer.size() - samples_in_buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: memcpy
|
||||||
|
for (size_t i = 0; i < new_span.size; i++) {
|
||||||
|
buffer.at(samples_in_buffer+i) = new_span[i];
|
||||||
|
}
|
||||||
|
samples_in_buffer += new_span.size;
|
||||||
|
} else if (buffer.empty() || samples_in_buffer == 0) {
|
||||||
|
// first pop might result in invalid state
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t desired_size {frame_length_ms * sample_rate * channels / 1000};
|
||||||
|
|
||||||
|
// > threshold?
|
||||||
|
if (samples_in_buffer < desired_size) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int16_t> return_buffer(desired_size);
|
||||||
|
// copy data
|
||||||
|
for (size_t i = 0; i < return_buffer.size(); i++) {
|
||||||
|
return_buffer.at(i) = buffer.at(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// now crop buffer (meh)
|
||||||
|
// move data from back to front
|
||||||
|
for (size_t i = 0; i < samples_in_buffer-return_buffer.size(); i++) {
|
||||||
|
buffer.at(i) = buffer.at(desired_size + i);
|
||||||
|
}
|
||||||
|
samples_in_buffer -= return_buffer.size();
|
||||||
|
|
||||||
|
return AudioFrame{
|
||||||
|
own_seq_counter++,
|
||||||
|
sample_rate,
|
||||||
|
channels,
|
||||||
|
std::move(return_buffer),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ToxAVCallAudioSink : public FrameStream2SinkI<AudioFrame> {
|
||||||
|
ToxAV& _toxav;
|
||||||
|
|
||||||
|
// bitrate for enabled state
|
||||||
|
uint32_t _audio_bitrate {32};
|
||||||
|
|
||||||
|
uint32_t _fid;
|
||||||
|
std::shared_ptr<QueuedFrameStream2<AudioFrame>> _writer;
|
||||||
|
|
||||||
|
ToxAVCallAudioSink(ToxAV& toxav, uint32_t fid) : _toxav(toxav), _fid(fid) {}
|
||||||
|
~ToxAVCallAudioSink(void) {
|
||||||
|
if (_writer) {
|
||||||
|
_writer = nullptr;
|
||||||
|
_toxav.toxavAudioSetBitRate(_fid, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sink
|
||||||
|
std::shared_ptr<FrameStream2I<AudioFrame>> subscribe(void) override {
|
||||||
|
if (_writer) {
|
||||||
|
// max 1 (exclusive for now)
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto err = _toxav.toxavAudioSetBitRate(_fid, _audio_bitrate);
|
||||||
|
if (err != TOXAV_ERR_BIT_RATE_SET_OK) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
_writer = std::make_shared<QueuedFrameStream2<AudioFrame>>(16, false);
|
||||||
|
|
||||||
|
return _writer;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool unsubscribe(const std::shared_ptr<FrameStream2I<AudioFrame>>& sub) override {
|
||||||
|
if (!sub || !_writer) {
|
||||||
|
// nah
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub == _writer) {
|
||||||
|
_writer = nullptr;
|
||||||
|
|
||||||
|
/*auto err = */_toxav.toxavAudioSetBitRate(_fid, 0);
|
||||||
|
// print warning? on error?
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// what
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
DebugToxCall::DebugToxCall(ObjectStore2& os, ToxAV& toxav, TextureUploaderI& tu) : _os(os), _toxav(toxav), _tu(tu) {
|
||||||
|
_toxav.subscribe(this, ToxAV_Event::friend_call);
|
||||||
|
_toxav.subscribe(this, ToxAV_Event::friend_call_state);
|
||||||
|
_toxav.subscribe(this, ToxAV_Event::friend_audio_bitrate);
|
||||||
|
_toxav.subscribe(this, ToxAV_Event::friend_video_bitrate);
|
||||||
|
_toxav.subscribe(this, ToxAV_Event::friend_audio_frame);
|
||||||
|
_toxav.subscribe(this, ToxAV_Event::friend_video_frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugToxCall::~DebugToxCall(void) {
|
||||||
|
// destroy all calls/connections/sources/sinks here
|
||||||
|
|
||||||
|
for (auto& [fid, call] : _calls) {
|
||||||
|
if (static_cast<bool>(call.incoming_vsrc)) {
|
||||||
|
_os.throwEventDestroy(call.incoming_vsrc);
|
||||||
|
call.incoming_vsrc.destroy();
|
||||||
|
}
|
||||||
|
if (static_cast<bool>(call.incoming_asrc)) {
|
||||||
|
_os.throwEventDestroy(call.incoming_asrc);
|
||||||
|
call.incoming_asrc.destroy();
|
||||||
|
}
|
||||||
|
if (static_cast<bool>(call.outgoing_vsink)) {
|
||||||
|
_os.throwEventDestroy(call.outgoing_vsink);
|
||||||
|
call.outgoing_vsink.destroy();
|
||||||
|
}
|
||||||
|
if (static_cast<bool>(call.outgoing_asink)) {
|
||||||
|
_os.throwEventDestroy(call.outgoing_asink);
|
||||||
|
call.outgoing_asink.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DebugToxCall::tick(float) {
|
||||||
|
// pump sinks to tox
|
||||||
|
// TODO: own thread or direct on push (requires thread save toxcore)
|
||||||
|
// TODO: pump at double the frame rate
|
||||||
|
for (const auto& [oc, vsink] : _os.registry().view<ToxAVCallVideoSink*>().each()) {
|
||||||
|
if (!vsink->_writer) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < 10; i++) {
|
||||||
|
auto new_frame_opt = vsink->_writer->pop();
|
||||||
|
if (!new_frame_opt.has_value()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!new_frame_opt.value().surface) {
|
||||||
|
// wtf?
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// conversion is done in the sinks stream
|
||||||
|
SDL_Surface* surf = new_frame_opt.value().surface.get();
|
||||||
|
assert(surf != nullptr);
|
||||||
|
|
||||||
|
SDL_LockSurface(surf);
|
||||||
|
_toxav.toxavVideoSendFrame(
|
||||||
|
vsink->_fid,
|
||||||
|
surf->w, surf->h,
|
||||||
|
static_cast<const uint8_t*>(surf->pixels),
|
||||||
|
static_cast<const uint8_t*>(surf->pixels) + surf->w * surf->h,
|
||||||
|
static_cast<const uint8_t*>(surf->pixels) + surf->w * surf->h + (surf->w/2) * (surf->h/2)
|
||||||
|
);
|
||||||
|
SDL_UnlockSurface(surf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& [oc, asink, asrf] : _os.registry().view<ToxAVCallAudioSink*, AudioStreamReFramer>().each()) {
|
||||||
|
if (!asink->_writer) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
asrf._stream = asink->_writer.get();
|
||||||
|
|
||||||
|
for (size_t i = 0; i < 10; i++) {
|
||||||
|
//auto new_frame_opt = asink->_writer->pop();
|
||||||
|
auto new_frame_opt = asrf.pop();
|
||||||
|
if (!new_frame_opt.has_value()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const auto& new_frame = new_frame_opt.value();
|
||||||
|
assert(new_frame.isS16());
|
||||||
|
|
||||||
|
//* @param sample_count Number of samples in this frame. Valid numbers here are
|
||||||
|
//* `((sample rate) * (audio length) / 1000)`, where audio length can be
|
||||||
|
//* 2.5, 5, 10, 20, 40 or 60 milliseconds.
|
||||||
|
|
||||||
|
// we likely needs to subdivide/repackage
|
||||||
|
// frame size should be an option exposed to the user
|
||||||
|
// with 10ms as a default ?
|
||||||
|
// the larger the frame size, the less overhead but the more delay
|
||||||
|
|
||||||
|
auto err = _toxav.toxavAudioSendFrame(
|
||||||
|
asink->_fid,
|
||||||
|
new_frame.getSpan<int16_t>().ptr,
|
||||||
|
new_frame.getSpan<int16_t>().size / new_frame.channels,
|
||||||
|
new_frame.channels,
|
||||||
|
new_frame.sample_rate
|
||||||
|
);
|
||||||
|
if (err != TOXAV_ERR_SEND_FRAME_OK) {
|
||||||
|
std::cerr << "DTC: failed to send audio frame " << err << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float DebugToxCall::render(void) {
|
||||||
|
float next_frame {2.f};
|
||||||
|
if (ImGui::Begin("toxav debug")) {
|
||||||
|
ImGui::Text("Calls:");
|
||||||
|
ImGui::Indent();
|
||||||
|
for (auto& [fid, call] : _calls) {
|
||||||
|
ImGui::PushID(fid);
|
||||||
|
|
||||||
|
ImGui::Text("fid:%d state:%d", fid, call.state);
|
||||||
|
if (call.incoming) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::SmallButton("answer")) {
|
||||||
|
const auto ret = _toxav.toxavAnswer(fid, 0, 0);
|
||||||
|
//const auto ret = _toxav.toxavAnswer(fid, 0, 1); // 1mbit/s
|
||||||
|
//const auto ret = _toxav.toxavAnswer(fid, 0, 2); // 2mbit/s
|
||||||
|
//const auto ret = _toxav.toxavAnswer(fid, 0, 100); // 100mbit/s
|
||||||
|
//const auto ret = _toxav.toxavAnswer(fid, 0, 2500); // 2500mbit/s
|
||||||
|
if (ret == TOXAV_ERR_ANSWER_OK) {
|
||||||
|
call.incoming = false;
|
||||||
|
|
||||||
|
// create sinks
|
||||||
|
call.outgoing_vsink = {_os.registry(), _os.registry().create()};
|
||||||
|
{
|
||||||
|
auto new_vsink = std::make_unique<ToxAVCallVideoSink>(_toxav, fid);
|
||||||
|
call.outgoing_vsink.emplace<ToxAVCallVideoSink*>(new_vsink.get());
|
||||||
|
call.outgoing_vsink.emplace<Components::FrameStream2Sink<SDLVideoFrame>>(std::move(new_vsink));
|
||||||
|
call.outgoing_vsink.emplace<Components::StreamSink>(Components::StreamSink::create<SDLVideoFrame>("ToxAV Friend Call Outgoing Video"));
|
||||||
|
_os.throwEventConstruct(call.outgoing_vsink);
|
||||||
|
}
|
||||||
|
call.outgoing_asink = {_os.registry(), _os.registry().create()};
|
||||||
|
{
|
||||||
|
auto new_asink = std::make_unique<ToxAVCallAudioSink>(_toxav, fid);
|
||||||
|
call.outgoing_asink.emplace<ToxAVCallAudioSink*>(new_asink.get());
|
||||||
|
call.outgoing_asink.emplace<AudioStreamReFramer>().frame_length_ms = 10;
|
||||||
|
call.outgoing_asink.emplace<Components::FrameStream2Sink<AudioFrame>>(std::move(new_asink));
|
||||||
|
call.outgoing_asink.emplace<Components::StreamSink>(Components::StreamSink::create<AudioFrame>("ToxAV Friend Call Outgoing Audio"));
|
||||||
|
_os.throwEventConstruct(call.outgoing_asink);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create sources
|
||||||
|
if (call.incoming_v) {
|
||||||
|
call.incoming_vsrc = {_os.registry(), _os.registry().create()};
|
||||||
|
{
|
||||||
|
auto new_vsrc = std::make_unique<SDLVideoFrameStream2MultiSource>();
|
||||||
|
call.incoming_vsrc.emplace<SDLVideoFrameStream2MultiSource*>(new_vsrc.get());
|
||||||
|
call.incoming_vsrc.emplace<Components::FrameStream2Source<SDLVideoFrame>>(std::move(new_vsrc));
|
||||||
|
call.incoming_vsrc.emplace<Components::StreamSource>(Components::StreamSource::create<SDLVideoFrame>("ToxAV Friend Call Incoming Video"));
|
||||||
|
_os.throwEventConstruct(call.incoming_vsrc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (call.incoming_a) {
|
||||||
|
call.incoming_asrc = {_os.registry(), _os.registry().create()};
|
||||||
|
{
|
||||||
|
auto new_asrc = std::make_unique<AudioFrameStream2MultiSource>();
|
||||||
|
call.incoming_asrc.emplace<AudioFrameStream2MultiSource*>(new_asrc.get());
|
||||||
|
call.incoming_asrc.emplace<Components::FrameStream2Source<AudioFrame>>(std::move(new_asrc));
|
||||||
|
call.incoming_asrc.emplace<Components::StreamSource>(Components::StreamSource::create<AudioFrame>("ToxAV Friend Call Incoming Audio"));
|
||||||
|
_os.throwEventConstruct(call.incoming_asrc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (call.state != TOXAV_FRIEND_CALL_STATE_FINISHED) {
|
||||||
|
next_frame = std::min(next_frame, 0.1f);
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::SmallButton("hang up")) {
|
||||||
|
const auto ret = _toxav.toxavCallControl(fid, TOXAV_CALL_CONTROL_CANCEL);
|
||||||
|
if (ret == TOXAV_ERR_CALL_CONTROL_OK) {
|
||||||
|
// we hung up
|
||||||
|
// not sure if its possible for toxcore to tell this us too when the other side does this at the same time?
|
||||||
|
call.state = TOXAV_FRIEND_CALL_STATE_FINISHED;
|
||||||
|
|
||||||
|
// TODO: stream manager disconnectAll()
|
||||||
|
if (static_cast<bool>(call.incoming_vsrc)) {
|
||||||
|
_os.throwEventDestroy(call.incoming_vsrc);
|
||||||
|
call.incoming_vsrc.destroy();
|
||||||
|
}
|
||||||
|
if (static_cast<bool>(call.incoming_asrc)) {
|
||||||
|
_os.throwEventDestroy(call.incoming_asrc);
|
||||||
|
call.incoming_asrc.destroy();
|
||||||
|
}
|
||||||
|
if (static_cast<bool>(call.outgoing_vsink)) {
|
||||||
|
_os.throwEventDestroy(call.outgoing_vsink);
|
||||||
|
call.outgoing_vsink.destroy();
|
||||||
|
}
|
||||||
|
if (static_cast<bool>(call.outgoing_asink)) {
|
||||||
|
_os.throwEventDestroy(call.outgoing_asink);
|
||||||
|
call.outgoing_asink.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//if (ImGui::BeginCombo("audio src", "---")) {
|
||||||
|
// ImGui::EndCombo();
|
||||||
|
//}
|
||||||
|
//if (ImGui::BeginCombo("video src", "---")) {
|
||||||
|
// ImGui::EndCombo();
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
ImGui::Unindent();
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
|
||||||
|
return next_frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DebugToxCall::onEvent(const Events::FriendCall& e) {
|
||||||
|
auto& call = _calls[e.friend_number];
|
||||||
|
call.incoming = true;
|
||||||
|
call.incoming_a = e.audio_enabled;
|
||||||
|
call.incoming_v = e.video_enabled;
|
||||||
|
call.state = TOXAV_FRIEND_CALL_STATE_NONE;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DebugToxCall::onEvent(const Events::FriendCallState& e) {
|
||||||
|
auto& call = _calls[e.friend_number];
|
||||||
|
call.state = e.state;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(call.state & TOXAV_FRIEND_CALL_STATE_FINISHED) != 0 ||
|
||||||
|
(call.state & TOXAV_FRIEND_CALL_STATE_ERROR) != 0
|
||||||
|
) {
|
||||||
|
if (static_cast<bool>(call.incoming_vsrc)) {
|
||||||
|
_os.throwEventDestroy(call.incoming_vsrc);
|
||||||
|
call.incoming_vsrc.destroy();
|
||||||
|
}
|
||||||
|
if (static_cast<bool>(call.incoming_asrc)) {
|
||||||
|
_os.throwEventDestroy(call.incoming_asrc);
|
||||||
|
call.incoming_asrc.destroy();
|
||||||
|
}
|
||||||
|
if (static_cast<bool>(call.outgoing_vsink)) {
|
||||||
|
_os.throwEventDestroy(call.outgoing_vsink);
|
||||||
|
call.outgoing_vsink.destroy();
|
||||||
|
}
|
||||||
|
if (static_cast<bool>(call.outgoing_asink)) {
|
||||||
|
_os.throwEventDestroy(call.outgoing_asink);
|
||||||
|
call.outgoing_asink.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DebugToxCall::onEvent(const Events::FriendAudioBitrate&) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DebugToxCall::onEvent(const Events::FriendVideoBitrate&) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DebugToxCall::onEvent(const Events::FriendAudioFrame& e) {
|
||||||
|
auto& call = _calls[e.friend_number];
|
||||||
|
|
||||||
|
if (!static_cast<bool>(call.incoming_asrc)) {
|
||||||
|
// missing src to put frame into ??
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(call.incoming_asrc.all_of<AudioFrameStream2MultiSource*>());
|
||||||
|
assert(call.incoming_asrc.all_of<Components::FrameStream2Source<AudioFrame>>());
|
||||||
|
|
||||||
|
call.num_a_frames++;
|
||||||
|
|
||||||
|
call.incoming_asrc.get<AudioFrameStream2MultiSource*>()->push(AudioFrame{
|
||||||
|
0, //seq
|
||||||
|
e.sampling_rate,
|
||||||
|
e.channels,
|
||||||
|
std::vector<int16_t>(e.pcm.begin(), e.pcm.end()) // copy
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DebugToxCall::onEvent(const Events::FriendVideoFrame& e) {
|
||||||
|
// TODO: skip if we dont know about this call
|
||||||
|
auto& call = _calls[e.friend_number];
|
||||||
|
|
||||||
|
if (!static_cast<bool>(call.incoming_vsrc)) {
|
||||||
|
// missing src to put frame into ??
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(call.incoming_vsrc.all_of<SDLVideoFrameStream2MultiSource*>());
|
||||||
|
assert(call.incoming_vsrc.all_of<Components::FrameStream2Source<SDLVideoFrame>>());
|
||||||
|
|
||||||
|
call.num_v_frames++;
|
||||||
|
|
||||||
|
auto* new_surf = SDL_CreateSurface(e.width, e.height, SDL_PIXELFORMAT_IYUV);
|
||||||
|
assert(new_surf);
|
||||||
|
if (SDL_LockSurface(new_surf)) {
|
||||||
|
// copy the data
|
||||||
|
// we know how the implementation works, its y u v consecutivlely
|
||||||
|
// y
|
||||||
|
for (size_t y = 0; y < e.height; y++) {
|
||||||
|
std::memcpy(
|
||||||
|
//static_cast<uint8_t*>(new_surf->pixels) + new_surf->pitch*y,
|
||||||
|
static_cast<uint8_t*>(new_surf->pixels) + e.width*y,
|
||||||
|
e.y.ptr + e.ystride*y,
|
||||||
|
e.width
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// u
|
||||||
|
for (size_t y = 0; y < e.height/2; y++) {
|
||||||
|
std::memcpy(
|
||||||
|
static_cast<uint8_t*>(new_surf->pixels) + (e.width*e.height) + (e.width/2)*y,
|
||||||
|
e.u.ptr + e.ustride*y,
|
||||||
|
e.width/2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// v
|
||||||
|
for (size_t y = 0; y < e.height/2; y++) {
|
||||||
|
std::memcpy(
|
||||||
|
static_cast<uint8_t*>(new_surf->pixels) + (e.width*e.height) + ((e.width/2)*(e.height/2)) + (e.width/2)*y,
|
||||||
|
e.v.ptr + e.vstride*y,
|
||||||
|
e.width/2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_UnlockSurface(new_surf);
|
||||||
|
}
|
||||||
|
|
||||||
|
call.incoming_vsrc.get<SDLVideoFrameStream2MultiSource*>()->push({
|
||||||
|
// ms -> us
|
||||||
|
Message::getTimeMS() * 1000, // TODO: make more precise
|
||||||
|
new_surf
|
||||||
|
});
|
||||||
|
|
||||||
|
SDL_DestroySurface(new_surf);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
52
src/debug_tox_call.hpp
Normal file
52
src/debug_tox_call.hpp
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
//#include <solanaceae/object_store/fwd.hpp>
|
||||||
|
#include <solanaceae/object_store/object_store.hpp>
|
||||||
|
#include "./tox_av.hpp"
|
||||||
|
#include "./texture_uploader.hpp"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
class DebugToxCall : public ToxAVEventI {
|
||||||
|
ObjectStore2& _os;
|
||||||
|
ToxAV& _toxav;
|
||||||
|
TextureUploaderI& _tu;
|
||||||
|
|
||||||
|
struct Call {
|
||||||
|
bool incoming {false};
|
||||||
|
bool incoming_a {false};
|
||||||
|
bool incoming_v {false};
|
||||||
|
|
||||||
|
uint32_t state {0}; // ? just last state ?
|
||||||
|
|
||||||
|
uint32_t incomming_abr {0};
|
||||||
|
uint32_t incomming_vbr {0};
|
||||||
|
|
||||||
|
size_t num_a_frames {0};
|
||||||
|
size_t num_v_frames {0};
|
||||||
|
|
||||||
|
ObjectHandle incoming_vsrc;
|
||||||
|
ObjectHandle incoming_asrc;
|
||||||
|
|
||||||
|
ObjectHandle outgoing_vsink;
|
||||||
|
ObjectHandle outgoing_asink;
|
||||||
|
};
|
||||||
|
// tox friend id -> call
|
||||||
|
std::map<uint32_t, Call> _calls;
|
||||||
|
|
||||||
|
public:
|
||||||
|
DebugToxCall(ObjectStore2& os, ToxAV& toxav, TextureUploaderI& tu);
|
||||||
|
~DebugToxCall(void);
|
||||||
|
|
||||||
|
void tick(float time_delta);
|
||||||
|
float render(void);
|
||||||
|
|
||||||
|
protected: // toxav events
|
||||||
|
bool onEvent(const Events::FriendCall&) override;
|
||||||
|
bool onEvent(const Events::FriendCallState&) override;
|
||||||
|
bool onEvent(const Events::FriendAudioBitrate&) override;
|
||||||
|
bool onEvent(const Events::FriendVideoBitrate&) override;
|
||||||
|
bool onEvent(const Events::FriendAudioFrame&) override;
|
||||||
|
bool onEvent(const Events::FriendVideoFrame&) override;
|
||||||
|
};
|
210
src/debug_video_tap.cpp
Normal file
210
src/debug_video_tap.cpp
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
#include "./debug_video_tap.hpp"
|
||||||
|
|
||||||
|
#include <solanaceae/object_store/object_store.hpp>
|
||||||
|
|
||||||
|
#include <entt/entity/entity.hpp>
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <imgui/imgui.h>
|
||||||
|
|
||||||
|
#include "./content/sdl_video_frame_stream2.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <memory>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
struct DebugVideoTapSink : public FrameStream2SinkI<SDLVideoFrame> {
|
||||||
|
std::shared_ptr<QueuedFrameStream2<SDLVideoFrame>> _writer;
|
||||||
|
|
||||||
|
DebugVideoTapSink(void) {}
|
||||||
|
~DebugVideoTapSink(void) {}
|
||||||
|
|
||||||
|
// sink
|
||||||
|
std::shared_ptr<FrameStream2I<SDLVideoFrame>> subscribe(void) override {
|
||||||
|
if (_writer) {
|
||||||
|
// max 1 (exclusive)
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
_writer = std::make_shared<QueuedFrameStream2<SDLVideoFrame>>(1, true);
|
||||||
|
|
||||||
|
return _writer;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool unsubscribe(const std::shared_ptr<FrameStream2I<SDLVideoFrame>>& sub) override {
|
||||||
|
if (!sub || !_writer) {
|
||||||
|
// nah
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub == _writer) {
|
||||||
|
_writer = nullptr;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// what
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
DebugVideoTap::DebugVideoTap(ObjectStore2& os, StreamManager& sm, TextureUploaderI& tu) : _os(os), _sm(sm), _tu(tu) {
|
||||||
|
// post self as video sink
|
||||||
|
_tap = {_os.registry(), _os.registry().create()};
|
||||||
|
try {
|
||||||
|
auto dvts = std::make_unique<DebugVideoTapSink>();
|
||||||
|
_tap.emplace<DebugVideoTapSink*>(dvts.get()); // to get our data back
|
||||||
|
_tap.emplace<Components::FrameStream2Sink<SDLVideoFrame>>(
|
||||||
|
std::move(dvts)
|
||||||
|
);
|
||||||
|
|
||||||
|
_tap.emplace<Components::StreamSink>(Components::StreamSink::create<SDLVideoFrame>("DebugVideoTap"));
|
||||||
|
} catch (...) {
|
||||||
|
_os.registry().destroy(_tap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugVideoTap::~DebugVideoTap(void) {
|
||||||
|
if (static_cast<bool>(_tap)) {
|
||||||
|
_os.registry().destroy(_tap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float DebugVideoTap::render(void) {
|
||||||
|
if (ImGui::Begin("DebugVideoTap")) {
|
||||||
|
{ // first pull the latest img from sink and update the texture
|
||||||
|
assert(static_cast<bool>(_tap));
|
||||||
|
|
||||||
|
auto& dvtsw = _tap.get<DebugVideoTapSink*>()->_writer;
|
||||||
|
if (dvtsw) {
|
||||||
|
while (true) {
|
||||||
|
auto new_frame_opt = dvtsw->pop();
|
||||||
|
if (new_frame_opt.has_value()) {
|
||||||
|
// timing
|
||||||
|
if (_v_last_ts == 0) {
|
||||||
|
_v_last_ts = new_frame_opt.value().timestampUS;
|
||||||
|
} else {
|
||||||
|
auto delta = int64_t(new_frame_opt.value().timestampUS) - int64_t(_v_last_ts);
|
||||||
|
_v_last_ts = new_frame_opt.value().timestampUS;
|
||||||
|
|
||||||
|
//delta = std::min<int64_t>(delta, 10*1000*1000);
|
||||||
|
|
||||||
|
if (_v_interval_avg == 0) {
|
||||||
|
_v_interval_avg = delta/1'000'000.f;
|
||||||
|
} else {
|
||||||
|
const float r = 0.2f;
|
||||||
|
_v_interval_avg = _v_interval_avg * (1.f-r) + (delta/1'000'000.f) * r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Surface* new_frame_surf = new_frame_opt.value().surface.get();
|
||||||
|
|
||||||
|
SDL_Surface* converted_surf = new_frame_surf;
|
||||||
|
if (new_frame_surf->format != SDL_PIXELFORMAT_RGBA32) {
|
||||||
|
// we need to convert
|
||||||
|
//std::cerr << "DVT: need to convert\n";
|
||||||
|
converted_surf = SDL_ConvertSurfaceAndColorspace(new_frame_surf, SDL_PIXELFORMAT_RGBA32, nullptr, SDL_COLORSPACE_RGB_DEFAULT, 0);
|
||||||
|
assert(converted_surf->format == SDL_PIXELFORMAT_RGBA32);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LockSurface(converted_surf);
|
||||||
|
if (_tex == 0 || (int)_tex_w != converted_surf->w || (int)_tex_h != converted_surf->h) {
|
||||||
|
_tu.destroy(_tex);
|
||||||
|
_tex = _tu.uploadRGBA(
|
||||||
|
static_cast<const uint8_t*>(converted_surf->pixels),
|
||||||
|
converted_surf->w,
|
||||||
|
converted_surf->h,
|
||||||
|
TextureUploaderI::LINEAR,
|
||||||
|
TextureUploaderI::STREAMING
|
||||||
|
);
|
||||||
|
|
||||||
|
_tex_w = converted_surf->w;
|
||||||
|
_tex_h = converted_surf->h;
|
||||||
|
} else {
|
||||||
|
_tu.updateRGBA(_tex, static_cast<const uint8_t*>(converted_surf->pixels), converted_surf->w * converted_surf->h * 4);
|
||||||
|
}
|
||||||
|
SDL_UnlockSurface(converted_surf);
|
||||||
|
|
||||||
|
if (new_frame_surf != converted_surf) {
|
||||||
|
// clean up temp
|
||||||
|
SDL_DestroySurface(converted_surf);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// list sources dropdown to connect too
|
||||||
|
std::string preview_label {"none"};
|
||||||
|
if (static_cast<bool>(_selected_src)) {
|
||||||
|
preview_label = std::to_string(entt::to_integral(entt::to_entity(_selected_src.entity()))) + " (" + _selected_src.get<Components::StreamSource>().name + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginCombo("selected source", preview_label.c_str())) {
|
||||||
|
if (ImGui::Selectable("none")) {
|
||||||
|
switchTo({});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& [oc, ss] : _os.registry().view<Components::StreamSource>().each()) {
|
||||||
|
if (ss.frame_type_name != entt::type_name<SDLVideoFrame>::value()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::string label = std::to_string(entt::to_integral(entt::to_entity(oc))) + " (" + ss.name + ")";
|
||||||
|
if (ImGui::Selectable(label.c_str())) {
|
||||||
|
switchTo({_os.registry(), oc});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndCombo();
|
||||||
|
}
|
||||||
|
|
||||||
|
//ImGui::SetNextItemWidth(0);
|
||||||
|
ImGui::Checkbox("mirror", &_mirror);
|
||||||
|
|
||||||
|
// img here
|
||||||
|
if (_tex != 0) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Text("moving avg interval: %f", _v_interval_avg);
|
||||||
|
const float img_w = ImGui::GetContentRegionAvail().x;
|
||||||
|
ImGui::Image(
|
||||||
|
reinterpret_cast<ImTextureID>(_tex),
|
||||||
|
ImVec2{img_w, img_w * float(_tex_h)/_tex_w},
|
||||||
|
ImVec2{_mirror?1.f:0.f, 0},
|
||||||
|
ImVec2{_mirror?0.f:1.f, 1}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
|
||||||
|
if (_v_interval_avg != 0) {
|
||||||
|
return _v_interval_avg;
|
||||||
|
} else {
|
||||||
|
return 2.f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DebugVideoTap::switchTo(ObjectHandle o) {
|
||||||
|
if (o == _selected_src) {
|
||||||
|
std::cerr << "DVT: switch to same ...\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_tu.destroy(_tex);
|
||||||
|
_tex = 0;
|
||||||
|
_v_last_ts = 0;
|
||||||
|
_v_interval_avg = 0;
|
||||||
|
|
||||||
|
if (static_cast<bool>(_selected_src)) {
|
||||||
|
_sm.disconnect<SDLVideoFrame>(_selected_src, _tap);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (static_cast<bool>(o) && _sm.connect<SDLVideoFrame>(o, _tap)) {
|
||||||
|
_selected_src = o;
|
||||||
|
} else {
|
||||||
|
std::cerr << "DVT: cleared video source\n";
|
||||||
|
_selected_src = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
34
src/debug_video_tap.hpp
Normal file
34
src/debug_video_tap.hpp
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <solanaceae/object_store/fwd.hpp>
|
||||||
|
#include "./stream_manager.hpp"
|
||||||
|
#include "./texture_uploader.hpp"
|
||||||
|
|
||||||
|
// provides a sink and a small window displaying a SDLVideoFrame
|
||||||
|
class DebugVideoTap {
|
||||||
|
ObjectStore2& _os;
|
||||||
|
StreamManager& _sm;
|
||||||
|
TextureUploaderI& _tu;
|
||||||
|
|
||||||
|
ObjectHandle _selected_src;
|
||||||
|
ObjectHandle _tap;
|
||||||
|
|
||||||
|
uint64_t _tex {0};
|
||||||
|
uint32_t _tex_w {0};
|
||||||
|
uint32_t _tex_h {0};
|
||||||
|
|
||||||
|
bool _mirror {false}; // flip horizontally
|
||||||
|
|
||||||
|
uint64_t _v_last_ts {0}; // us
|
||||||
|
float _v_interval_avg {0.f}; // s
|
||||||
|
|
||||||
|
public:
|
||||||
|
DebugVideoTap(ObjectStore2& os, StreamManager& sm, TextureUploaderI& tu);
|
||||||
|
~DebugVideoTap(void);
|
||||||
|
|
||||||
|
float render(void);
|
||||||
|
|
||||||
|
void switchTo(ObjectHandle o);
|
||||||
|
};
|
||||||
|
|
46
src/main.cpp
46
src/main.cpp
@ -12,6 +12,9 @@
|
|||||||
|
|
||||||
#include "./start_screen.hpp"
|
#include "./start_screen.hpp"
|
||||||
|
|
||||||
|
#include "./content/sdl_video_frame_stream2.hpp"
|
||||||
|
#include "./content/sdl_audio_frame_stream2.hpp"
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
@ -35,7 +38,7 @@ int main(int argc, char** argv) {
|
|||||||
|
|
||||||
// setup hints
|
// setup hints
|
||||||
#ifndef __ANDROID__
|
#ifndef __ANDROID__
|
||||||
if (SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1") != SDL_TRUE) {
|
if (!SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1")) {
|
||||||
std::cerr << "Failed to set '" << SDL_HINT_VIDEO_ALLOW_SCREENSAVER << "' to 1\n";
|
std::cerr << "Failed to set '" << SDL_HINT_VIDEO_ALLOW_SCREENSAVER << "' to 1\n";
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -72,10 +75,51 @@ int main(int argc, char** argv) {
|
|||||||
|
|
||||||
std::cout << "SDL Renderer: " << SDL_GetRendererName(renderer.get()) << "\n";
|
std::cout << "SDL Renderer: " << SDL_GetRendererName(renderer.get()) << "\n";
|
||||||
|
|
||||||
|
// optionally init audio and camera
|
||||||
|
if (!SDL_Init(SDL_INIT_AUDIO)) {
|
||||||
|
std::cerr << "SDL_Init AUDIO failed (" << SDL_GetError() << ")\n";
|
||||||
|
} else if (false) {
|
||||||
|
SDLAudioInputDevice aid;
|
||||||
|
auto reader = aid.subscribe();
|
||||||
|
|
||||||
|
auto writer = SDLAudioOutputDeviceDefaultSink{}.subscribe();
|
||||||
|
|
||||||
|
for (size_t i = 0; i < 200; i++) {
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
auto new_frame_opt = reader->pop();
|
||||||
|
if (new_frame_opt.has_value()) {
|
||||||
|
std::cout << "audio frame was seq:" << new_frame_opt.value().seq << " sr:" << new_frame_opt.value().sample_rate << " " << (new_frame_opt.value().isS16()?"S16":"F32") << " l:" << (new_frame_opt.value().isS16()?new_frame_opt.value().getSpan<int16_t>().size:new_frame_opt.value().getSpan<float>().size) << "\n";
|
||||||
|
writer->push(new_frame_opt.value());
|
||||||
|
} else {
|
||||||
|
std::cout << "no audio frame\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aid.unsubscribe(reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SDL_Init(SDL_INIT_CAMERA)) {
|
||||||
|
std::cerr << "SDL_Init CAMERA failed (" << SDL_GetError() << ")\n";
|
||||||
|
} else if (false) { // HACK
|
||||||
|
std::cerr << "CAMERA initialized\n";
|
||||||
|
SDLVideoCameraContent vcc;
|
||||||
|
auto reader = vcc.subscribe();
|
||||||
|
for (size_t i = 0; i < 20; i++) {
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
auto new_frame_opt = reader->pop();
|
||||||
|
if (new_frame_opt.has_value()) {
|
||||||
|
std::cout << "video frame was " << new_frame_opt.value().surface->w << "x" << new_frame_opt.value().surface->h << " " << new_frame_opt.value().timestampUS << "us " << new_frame_opt.value().surface->format << "sf\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vcc.unsubscribe(reader);
|
||||||
|
}
|
||||||
|
std::cout << "after sdl video stuffery\n";
|
||||||
|
|
||||||
IMGUI_CHECKVERSION();
|
IMGUI_CHECKVERSION();
|
||||||
ImGui::CreateContext();
|
ImGui::CreateContext();
|
||||||
|
|
||||||
// TODO: test android behaviour
|
// TODO: test android behaviour
|
||||||
|
// -> its too big, dpi does not take eye-screen-distance into account
|
||||||
float display_scale = SDL_GetWindowDisplayScale(window.get());
|
float display_scale = SDL_GetWindowDisplayScale(window.get());
|
||||||
if (display_scale < 0.001f) {
|
if (display_scale < 0.001f) {
|
||||||
// error?
|
// error?
|
||||||
|
@ -9,6 +9,11 @@
|
|||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include "./content/sdl_video_frame_stream2.hpp"
|
||||||
|
#include "content/audio_stream.hpp"
|
||||||
|
#include "content/sdl_audio_frame_stream2.hpp"
|
||||||
|
#include "stream_manager.hpp"
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
@ -19,11 +24,13 @@ MainScreen::MainScreen(SimpleConfigModel&& conf_, SDL_Renderer* renderer_, Theme
|
|||||||
rmm(cr),
|
rmm(cr),
|
||||||
msnj{cr, {}, {}},
|
msnj{cr, {}, {}},
|
||||||
mts(rmm),
|
mts(rmm),
|
||||||
|
sm(os),
|
||||||
tc(save_path, save_password),
|
tc(save_path, save_password),
|
||||||
tpi(tc.getTox()),
|
tpi(tc.getTox()),
|
||||||
ad(tc),
|
ad(tc),
|
||||||
#if TOMATO_TOX_AV
|
#if TOMATO_TOX_AV
|
||||||
tav(tc.getTox()),
|
tav(tc.getTox()),
|
||||||
|
dtc(os, tav, sdlrtu),
|
||||||
#endif
|
#endif
|
||||||
tcm(cr, tc, tc),
|
tcm(cr, tc, tc),
|
||||||
tmm(rmm, cr, tcm, tc, tc),
|
tmm(rmm, cr, tcm, tc, tc),
|
||||||
@ -40,6 +47,8 @@ MainScreen::MainScreen(SimpleConfigModel&& conf_, SDL_Renderer* renderer_, Theme
|
|||||||
cg(conf, os, rmm, cr, sdlrtu, contact_tc, msg_tc, theme),
|
cg(conf, os, rmm, cr, sdlrtu, contact_tc, msg_tc, theme),
|
||||||
sw(conf),
|
sw(conf),
|
||||||
osui(os),
|
osui(os),
|
||||||
|
smui(os, sm),
|
||||||
|
dvt(os, sm, sdlrtu),
|
||||||
tuiu(tc, conf),
|
tuiu(tc, conf),
|
||||||
tdch(tpi)
|
tdch(tpi)
|
||||||
{
|
{
|
||||||
@ -136,6 +145,53 @@ MainScreen::MainScreen(SimpleConfigModel&& conf_, SDL_Renderer* renderer_, Theme
|
|||||||
}
|
}
|
||||||
|
|
||||||
conf.dump();
|
conf.dump();
|
||||||
|
|
||||||
|
{ // add system av devices
|
||||||
|
if (false) {
|
||||||
|
ObjectHandle vsrc {os.registry(), os.registry().create()};
|
||||||
|
try {
|
||||||
|
vsrc.emplace<Components::FrameStream2Source<SDLVideoFrame>>(
|
||||||
|
std::make_unique<SDLVideoCameraContent>()
|
||||||
|
);
|
||||||
|
|
||||||
|
vsrc.emplace<Components::StreamSource>(Components::StreamSource::create<SDLVideoFrame>("WebCam"));
|
||||||
|
|
||||||
|
os.throwEventConstruct(vsrc);
|
||||||
|
} catch (...) {
|
||||||
|
os.registry().destroy(vsrc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (true) { // audio in
|
||||||
|
ObjectHandle asrc {os.registry(), os.registry().create()};
|
||||||
|
try {
|
||||||
|
asrc.emplace<Components::FrameStream2Source<AudioFrame>>(
|
||||||
|
std::make_unique<SDLAudioInputDevice>()
|
||||||
|
);
|
||||||
|
|
||||||
|
asrc.emplace<Components::StreamSource>(Components::StreamSource::create<AudioFrame>("SDL Audio Default Recording Device"));
|
||||||
|
|
||||||
|
os.throwEventConstruct(asrc);
|
||||||
|
} catch (...) {
|
||||||
|
os.registry().destroy(asrc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // audio out
|
||||||
|
ObjectHandle asink {os.registry(), os.registry().create()};
|
||||||
|
try {
|
||||||
|
asink.emplace<Components::FrameStream2Sink<AudioFrame>>(
|
||||||
|
std::make_unique<SDLAudioOutputDeviceDefaultSink>()
|
||||||
|
);
|
||||||
|
|
||||||
|
asink.emplace<Components::StreamSink>(Components::StreamSink::create<AudioFrame>("SDL Audio Default Playback Device"));
|
||||||
|
|
||||||
|
os.throwEventConstruct(asink);
|
||||||
|
} catch (...) {
|
||||||
|
os.registry().destroy(asink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MainScreen::~MainScreen(void) {
|
MainScreen::~MainScreen(void) {
|
||||||
@ -252,14 +308,19 @@ Screen* MainScreen::render(float time_delta, bool&) {
|
|||||||
}
|
}
|
||||||
// ACTUALLY NOT IF RENDERED, MOVED LOGIC TO ABOVE
|
// ACTUALLY NOT IF RENDERED, MOVED LOGIC TO ABOVE
|
||||||
// it might unload textures, so it needs to be done before rendering
|
// it might unload textures, so it needs to be done before rendering
|
||||||
const float ctc_interval = contact_tc.update();
|
float animation_interval = contact_tc.update();
|
||||||
const float msgtc_interval = msg_tc.update();
|
animation_interval = std::min<float>(animation_interval, msg_tc.update());
|
||||||
|
|
||||||
const float cg_interval = cg.render(time_delta); // render
|
const float cg_interval = cg.render(time_delta); // render
|
||||||
sw.render(); // render
|
sw.render(); // render
|
||||||
osui.render();
|
osui.render();
|
||||||
|
smui.render();
|
||||||
|
animation_interval = std::min<float>(animation_interval, dvt.render());
|
||||||
tuiu.render(); // render
|
tuiu.render(); // render
|
||||||
tdch.render(); // render
|
tdch.render(); // render
|
||||||
|
#if TOMATO_TOX_AV
|
||||||
|
animation_interval = std::min<float>(animation_interval, dtc.render());
|
||||||
|
#endif
|
||||||
|
|
||||||
{ // main window menubar injection
|
{ // main window menubar injection
|
||||||
if (ImGui::Begin("tomato")) {
|
if (ImGui::Begin("tomato")) {
|
||||||
@ -440,8 +501,7 @@ Screen* MainScreen::render(float time_delta, bool&) {
|
|||||||
|
|
||||||
// low delay time window
|
// low delay time window
|
||||||
if (!_window_hidden && _time_since_event < curr_profile.low_delay_window) {
|
if (!_window_hidden && _time_since_event < curr_profile.low_delay_window) {
|
||||||
_render_interval = std::min<float>(_render_interval, ctc_interval);
|
_render_interval = std::min<float>(_render_interval, animation_interval);
|
||||||
_render_interval = std::min<float>(_render_interval, msgtc_interval);
|
|
||||||
|
|
||||||
_render_interval = std::clamp(
|
_render_interval = std::clamp(
|
||||||
_render_interval,
|
_render_interval,
|
||||||
@ -450,8 +510,7 @@ Screen* MainScreen::render(float time_delta, bool&) {
|
|||||||
);
|
);
|
||||||
// mid delay time window
|
// mid delay time window
|
||||||
} else if (!_window_hidden && _time_since_event < curr_profile.mid_delay_window) {
|
} else if (!_window_hidden && _time_since_event < curr_profile.mid_delay_window) {
|
||||||
_render_interval = std::min<float>(_render_interval, ctc_interval);
|
_render_interval = std::min<float>(_render_interval, animation_interval);
|
||||||
_render_interval = std::min<float>(_render_interval, msgtc_interval);
|
|
||||||
|
|
||||||
_render_interval = std::clamp(
|
_render_interval = std::clamp(
|
||||||
_render_interval,
|
_render_interval,
|
||||||
@ -476,8 +535,16 @@ Screen* MainScreen::render(float time_delta, bool&) {
|
|||||||
Screen* MainScreen::tick(float time_delta, bool& quit) {
|
Screen* MainScreen::tick(float time_delta, bool& quit) {
|
||||||
quit = !tc.iterate(time_delta); // compute
|
quit = !tc.iterate(time_delta); // compute
|
||||||
|
|
||||||
|
#if TOMATO_TOX_AV
|
||||||
|
tav.toxavIterate();
|
||||||
|
const float av_interval = tav.toxavIterationInterval()/1000.f;
|
||||||
|
dtc.tick(time_delta);
|
||||||
|
#endif
|
||||||
|
|
||||||
tcm.iterate(time_delta); // compute
|
tcm.iterate(time_delta); // compute
|
||||||
|
|
||||||
|
const float sm_interval = sm.tick(time_delta);
|
||||||
|
|
||||||
const float fo_interval = tffom.tick(time_delta);
|
const float fo_interval = tffom.tick(time_delta);
|
||||||
|
|
||||||
tam.iterate(); // compute
|
tam.iterate(); // compute
|
||||||
@ -510,6 +577,18 @@ Screen* MainScreen::tick(float time_delta, bool& quit) {
|
|||||||
fo_interval
|
fo_interval
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#if TOMATO_TOX_AV
|
||||||
|
_min_tick_interval = std::min<float>(
|
||||||
|
_min_tick_interval,
|
||||||
|
av_interval
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
_min_tick_interval = std::min<float>(
|
||||||
|
_min_tick_interval,
|
||||||
|
sm_interval
|
||||||
|
);
|
||||||
|
|
||||||
//std::cout << "MS: min tick interval: " << _min_tick_interval << "\n";
|
//std::cout << "MS: min tick interval: " << _min_tick_interval << "\n";
|
||||||
|
|
||||||
switch (_compute_perf_mode) {
|
switch (_compute_perf_mode) {
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
#include <solanaceae/tox_messages/tox_message_manager.hpp>
|
#include <solanaceae/tox_messages/tox_message_manager.hpp>
|
||||||
#include <solanaceae/tox_messages/tox_transfer_manager.hpp>
|
#include <solanaceae/tox_messages/tox_transfer_manager.hpp>
|
||||||
|
|
||||||
|
#include "./stream_manager.hpp"
|
||||||
|
|
||||||
#include "./tox_client.hpp"
|
#include "./tox_client.hpp"
|
||||||
#include "./auto_dirty.hpp"
|
#include "./auto_dirty.hpp"
|
||||||
|
|
||||||
@ -30,12 +32,15 @@
|
|||||||
#include "./chat_gui4.hpp"
|
#include "./chat_gui4.hpp"
|
||||||
#include "./chat_gui/settings_window.hpp"
|
#include "./chat_gui/settings_window.hpp"
|
||||||
#include "./object_store_ui.hpp"
|
#include "./object_store_ui.hpp"
|
||||||
|
#include "./stream_manager_ui.hpp"
|
||||||
|
#include "./debug_video_tap.hpp"
|
||||||
#include "./tox_ui_utils.hpp"
|
#include "./tox_ui_utils.hpp"
|
||||||
#include "./tox_dht_cap_histo.hpp"
|
#include "./tox_dht_cap_histo.hpp"
|
||||||
#include "./tox_friend_faux_offline_messaging.hpp"
|
#include "./tox_friend_faux_offline_messaging.hpp"
|
||||||
|
|
||||||
#if TOMATO_TOX_AV
|
#if TOMATO_TOX_AV
|
||||||
#include "./tox_av.hpp"
|
#include "./tox_av.hpp"
|
||||||
|
#include "./debug_tox_call.hpp"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
@ -58,12 +63,15 @@ struct MainScreen final : public Screen {
|
|||||||
MessageSerializerNJ msnj;
|
MessageSerializerNJ msnj;
|
||||||
MessageTimeSort mts;
|
MessageTimeSort mts;
|
||||||
|
|
||||||
|
StreamManager sm;
|
||||||
|
|
||||||
ToxEventLogger tel{std::cout};
|
ToxEventLogger tel{std::cout};
|
||||||
ToxClient tc;
|
ToxClient tc;
|
||||||
ToxPrivateImpl tpi;
|
ToxPrivateImpl tpi;
|
||||||
AutoDirty ad;
|
AutoDirty ad;
|
||||||
#if TOMATO_TOX_AV
|
#if TOMATO_TOX_AV
|
||||||
ToxAV tav;
|
ToxAV tav;
|
||||||
|
DebugToxCall dtc;
|
||||||
#endif
|
#endif
|
||||||
ToxContactModel2 tcm;
|
ToxContactModel2 tcm;
|
||||||
ToxMessageManager tmm;
|
ToxMessageManager tmm;
|
||||||
@ -86,6 +94,8 @@ struct MainScreen final : public Screen {
|
|||||||
ChatGui4 cg;
|
ChatGui4 cg;
|
||||||
SettingsWindow sw;
|
SettingsWindow sw;
|
||||||
ObjectStoreUI osui;
|
ObjectStoreUI osui;
|
||||||
|
StreamManagerUI smui;
|
||||||
|
DebugVideoTap dvt;
|
||||||
ToxUIUtils tuiu;
|
ToxUIUtils tuiu;
|
||||||
ToxDHTCapHisto tdch;
|
ToxDHTCapHisto tdch;
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ uint64_t SDLRendererTextureUploader::uploadRGBA(const uint8_t* data, uint32_t wi
|
|||||||
SDL_UpdateTexture(tex, nullptr, surf->pixels, surf->pitch);
|
SDL_UpdateTexture(tex, nullptr, surf->pixels, surf->pitch);
|
||||||
|
|
||||||
SDL_BlendMode surf_blend_mode = SDL_BLENDMODE_NONE;
|
SDL_BlendMode surf_blend_mode = SDL_BLENDMODE_NONE;
|
||||||
if (SDL_GetSurfaceBlendMode(surf, &surf_blend_mode) == 0) {
|
if (SDL_GetSurfaceBlendMode(surf, &surf_blend_mode)) {
|
||||||
SDL_SetTextureBlendMode(tex, surf_blend_mode);
|
SDL_SetTextureBlendMode(tex, surf_blend_mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
320
src/stream_manager.hpp
Normal file
320
src/stream_manager.hpp
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <solanaceae/object_store/fwd.hpp>
|
||||||
|
#include <solanaceae/object_store/object_store.hpp>
|
||||||
|
|
||||||
|
#include <entt/core/type_info.hpp>
|
||||||
|
|
||||||
|
#include "./content/frame_stream2.hpp"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <thread>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
// fwd
|
||||||
|
class StreamManager;
|
||||||
|
|
||||||
|
namespace Components {
|
||||||
|
struct StreamSource {
|
||||||
|
std::string name;
|
||||||
|
std::string frame_type_name;
|
||||||
|
|
||||||
|
std::function<bool(StreamManager&, Object, Object, bool)> connect_fn;
|
||||||
|
|
||||||
|
template<typename FrameType>
|
||||||
|
static StreamSource create(const std::string& name);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StreamSink {
|
||||||
|
std::string name;
|
||||||
|
std::string frame_type_name;
|
||||||
|
|
||||||
|
template<typename FrameType>
|
||||||
|
static StreamSink create(const std::string& name);
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename FrameType>
|
||||||
|
using FrameStream2Source = std::unique_ptr<FrameStream2SourceI<FrameType>>;
|
||||||
|
|
||||||
|
template<typename FrameType>
|
||||||
|
using FrameStream2Sink = std::unique_ptr<FrameStream2SinkI<FrameType>>;
|
||||||
|
|
||||||
|
} // Components
|
||||||
|
|
||||||
|
|
||||||
|
class StreamManager {
|
||||||
|
friend class StreamManagerUI; // TODO: make this go away
|
||||||
|
ObjectStore2& _os;
|
||||||
|
|
||||||
|
struct Connection {
|
||||||
|
ObjectHandle src;
|
||||||
|
ObjectHandle sink;
|
||||||
|
|
||||||
|
struct Data {
|
||||||
|
virtual ~Data(void) {}
|
||||||
|
};
|
||||||
|
std::unique_ptr<Data> data; // stores reader writer type erased
|
||||||
|
std::function<void(Connection&)> pump_fn;
|
||||||
|
std::function<void(Connection&)> unsubscribe_fn;
|
||||||
|
|
||||||
|
bool on_main_thread {true};
|
||||||
|
std::atomic_bool stop {false}; // disconnect
|
||||||
|
std::atomic_bool finished {false}; // disconnect
|
||||||
|
|
||||||
|
// pump thread
|
||||||
|
std::thread pump_thread;
|
||||||
|
|
||||||
|
// frame interval counters and estimates
|
||||||
|
|
||||||
|
Connection(void) = default;
|
||||||
|
Connection(
|
||||||
|
ObjectHandle src_,
|
||||||
|
ObjectHandle sink_,
|
||||||
|
std::unique_ptr<Data>&& data_,
|
||||||
|
std::function<void(Connection&)>&& pump_fn_,
|
||||||
|
std::function<void(Connection&)>&& unsubscribe_fn_,
|
||||||
|
bool on_main_thread_ = true
|
||||||
|
) :
|
||||||
|
src(src_),
|
||||||
|
sink(sink_),
|
||||||
|
data(std::move(data_)),
|
||||||
|
pump_fn(std::move(pump_fn_)),
|
||||||
|
unsubscribe_fn(std::move(unsubscribe_fn_)),
|
||||||
|
on_main_thread(on_main_thread_)
|
||||||
|
{
|
||||||
|
if (!on_main_thread) {
|
||||||
|
// start thread
|
||||||
|
pump_thread = std::thread([this](void) {
|
||||||
|
while (!stop) {
|
||||||
|
pump_fn(*this);
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||||
|
}
|
||||||
|
finished = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
std::vector<std::unique_ptr<Connection>> _connections;
|
||||||
|
|
||||||
|
public:
|
||||||
|
StreamManager(ObjectStore2& os) : _os(os) {}
|
||||||
|
virtual ~StreamManager(void) {
|
||||||
|
// stop all connetions
|
||||||
|
for (const auto& con : _connections) {
|
||||||
|
con->stop = true;
|
||||||
|
if (!con->on_main_thread) {
|
||||||
|
con->pump_thread.join(); // we skip the finished check and wait
|
||||||
|
}
|
||||||
|
con->unsubscribe_fn(*con);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stream type is FrameStream2I<FrameType>
|
||||||
|
// TODO: improve this design
|
||||||
|
// src and sink need to be a FrameStream2MultiStream<FrameType>
|
||||||
|
template<typename FrameType>
|
||||||
|
bool connect(Object src, Object sink, bool threaded = true) {
|
||||||
|
auto res = std::find_if(
|
||||||
|
_connections.cbegin(), _connections.cend(),
|
||||||
|
[&](const auto& a) { return a->src == src && a->sink == sink; }
|
||||||
|
);
|
||||||
|
if (res != _connections.cend()) {
|
||||||
|
// already exists
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto h_src = _os.objectHandle(src);
|
||||||
|
auto h_sink = _os.objectHandle(sink);
|
||||||
|
if (!static_cast<bool>(h_src) || !static_cast<bool>(h_sink)) {
|
||||||
|
// an object does not exist
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!h_src.all_of<Components::FrameStream2Source<FrameType>>()) {
|
||||||
|
// src not stream source
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!h_sink.all_of<Components::FrameStream2Sink<FrameType>>()) {
|
||||||
|
// sink not stream sink
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& src_stream = h_src.get<Components::FrameStream2Source<FrameType>>();
|
||||||
|
auto& sink_stream = h_sink.get<Components::FrameStream2Sink<FrameType>>();
|
||||||
|
|
||||||
|
struct inlineData : public Connection::Data {
|
||||||
|
virtual ~inlineData(void) {}
|
||||||
|
std::shared_ptr<FrameStream2I<FrameType>> reader;
|
||||||
|
std::shared_ptr<FrameStream2I<FrameType>> writer;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto our_data = std::make_unique<inlineData>();
|
||||||
|
|
||||||
|
our_data->reader = src_stream->subscribe();
|
||||||
|
if (!our_data->reader) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
our_data->writer = sink_stream->subscribe();
|
||||||
|
if (!our_data->writer) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_connections.push_back(std::make_unique<Connection>(
|
||||||
|
h_src,
|
||||||
|
h_sink,
|
||||||
|
std::move(our_data),
|
||||||
|
[](Connection& con) -> void {
|
||||||
|
// there might be more stored
|
||||||
|
for (size_t i = 0; i < 10; i++) {
|
||||||
|
auto new_frame_opt = static_cast<inlineData*>(con.data.get())->reader->pop();
|
||||||
|
// TODO: frame interval estimates
|
||||||
|
if (new_frame_opt.has_value()) {
|
||||||
|
static_cast<inlineData*>(con.data.get())->writer->push(new_frame_opt.value());
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[](Connection& con) -> void {
|
||||||
|
auto* src_stream_ptr = con.src.try_get<Components::FrameStream2Source<FrameType>>();
|
||||||
|
if (src_stream_ptr != nullptr) {
|
||||||
|
(*src_stream_ptr)->unsubscribe(static_cast<inlineData*>(con.data.get())->reader);
|
||||||
|
}
|
||||||
|
auto* sink_stream_ptr = con.sink.try_get<Components::FrameStream2Sink<FrameType>>();
|
||||||
|
if (sink_stream_ptr != nullptr) {
|
||||||
|
(*sink_stream_ptr)->unsubscribe(static_cast<inlineData*>(con.data.get())->writer);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
!threaded
|
||||||
|
));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool connect(Object src, Object sink, bool threaded = true) {
|
||||||
|
auto h_src = _os.objectHandle(src);
|
||||||
|
auto h_sink = _os.objectHandle(sink);
|
||||||
|
if (!static_cast<bool>(h_src) || !static_cast<bool>(h_sink)) {
|
||||||
|
// an object does not exist
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get src and sink comps
|
||||||
|
if (!h_src.all_of<Components::StreamSource>()) {
|
||||||
|
// src not stream source
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!h_sink.all_of<Components::StreamSink>()) {
|
||||||
|
// sink not stream sink
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& ssrc = h_src.get<Components::StreamSource>();
|
||||||
|
const auto& ssink = h_sink.get<Components::StreamSink>();
|
||||||
|
|
||||||
|
// compare type
|
||||||
|
if (ssrc.frame_type_name != ssink.frame_type_name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// always fail in debug mode
|
||||||
|
assert(static_cast<bool>(ssrc.connect_fn));
|
||||||
|
if (!static_cast<bool>(ssrc.connect_fn)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// use connect fn from src
|
||||||
|
return ssrc.connect_fn(*this, src, sink, threaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename StreamType>
|
||||||
|
bool disconnect(Object src, Object sink) {
|
||||||
|
auto res = std::find_if(
|
||||||
|
_connections.cbegin(), _connections.cend(),
|
||||||
|
[&](const auto& a) { return a->src == src && a->sink == sink; }
|
||||||
|
);
|
||||||
|
if (res == _connections.cend()) {
|
||||||
|
// not found
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// do disconnect
|
||||||
|
(*res)->stop = true;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename StreamType>
|
||||||
|
bool disconnectAll(Object o) {
|
||||||
|
bool succ {false};
|
||||||
|
for (const auto& con : _connections) {
|
||||||
|
if (con->src == o || con->sink == o) {
|
||||||
|
con->stop = true;
|
||||||
|
succ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return succ;
|
||||||
|
}
|
||||||
|
|
||||||
|
// do we need the time delta?
|
||||||
|
float tick(float) {
|
||||||
|
// pump all mainthread connections
|
||||||
|
for (auto it = _connections.begin(); it != _connections.end();) {
|
||||||
|
auto& con = **it;
|
||||||
|
|
||||||
|
if (!static_cast<bool>(con.src) || !static_cast<bool>(con.sink)) {
|
||||||
|
// either side disappeard without disconnectAll
|
||||||
|
// TODO: warn/error log
|
||||||
|
con.stop = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (con.on_main_thread) {
|
||||||
|
con.pump_fn(con);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (con.stop && (con.finished || con.on_main_thread)) {
|
||||||
|
if (!con.on_main_thread) {
|
||||||
|
assert(con.pump_thread.joinable());
|
||||||
|
con.pump_thread.join();
|
||||||
|
}
|
||||||
|
con.unsubscribe_fn(con);
|
||||||
|
it = _connections.erase(it);
|
||||||
|
} else {
|
||||||
|
it++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return min over intervals instead
|
||||||
|
return 0.01f;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace Components {
|
||||||
|
|
||||||
|
// we require the complete sm type here
|
||||||
|
template<typename FrameType>
|
||||||
|
StreamSource StreamSource::create(const std::string& name) {
|
||||||
|
return StreamSource{
|
||||||
|
name,
|
||||||
|
std::string{entt::type_name<FrameType>::value()},
|
||||||
|
+[](StreamManager& sm, Object src, Object sink, bool threaded) {
|
||||||
|
return sm.connect<FrameType>(src, sink, threaded);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename FrameType>
|
||||||
|
StreamSink StreamSink::create(const std::string& name) {
|
||||||
|
return StreamSink{
|
||||||
|
name,
|
||||||
|
std::string{entt::type_name<FrameType>::value()},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // Components
|
||||||
|
|
222
src/stream_manager_ui.cpp
Normal file
222
src/stream_manager_ui.cpp
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
#include "./stream_manager_ui.hpp"
|
||||||
|
|
||||||
|
#include <solanaceae/object_store/object_store.hpp>
|
||||||
|
|
||||||
|
#include <imgui/imgui.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
StreamManagerUI::StreamManagerUI(ObjectStore2& os, StreamManager& sm) : _os(os), _sm(sm) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void StreamManagerUI::render(void) {
|
||||||
|
{ // main window menubar injection
|
||||||
|
// assumes the window "tomato" was rendered already by cg
|
||||||
|
if (ImGui::Begin("tomato")) {
|
||||||
|
if (ImGui::BeginMenuBar()) {
|
||||||
|
// TODO: drop all menu sep?
|
||||||
|
//ImGui::Separator(); // os already exists (very hacky)
|
||||||
|
if (ImGui::BeginMenu("ObjectStore")) {
|
||||||
|
if (ImGui::MenuItem("Stream Manger", nullptr, _show_window)) {
|
||||||
|
_show_window = !_show_window;
|
||||||
|
}
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
|
ImGui::EndMenuBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!_show_window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::Begin("StreamManagerUI", &_show_window)) {
|
||||||
|
// TODO: node canvas
|
||||||
|
|
||||||
|
// by fametype ??
|
||||||
|
|
||||||
|
if (ImGui::CollapsingHeader("Sources", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||||
|
// list sources
|
||||||
|
if (ImGui::BeginTable("sources_and_sinks", 4, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_BordersInnerV)) {
|
||||||
|
ImGui::TableSetupColumn("id");
|
||||||
|
ImGui::TableSetupColumn("name");
|
||||||
|
ImGui::TableSetupColumn("##conn");
|
||||||
|
ImGui::TableSetupColumn("type");
|
||||||
|
|
||||||
|
ImGui::TableHeadersRow();
|
||||||
|
|
||||||
|
for (const auto& [oc, ss] : _os.registry().view<Components::StreamSource>().each()) {
|
||||||
|
//ImGui::Text("src %d (%s)[%s]", entt::to_integral(entt::to_entity(oc)), ss.name.c_str(), ss.frame_type_name.c_str());
|
||||||
|
ImGui::PushID(entt::to_integral(oc));
|
||||||
|
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::Text("%d", entt::to_integral(entt::to_entity(oc)));
|
||||||
|
|
||||||
|
const auto *ssrc = _os.registry().try_get<Components::StreamSource>(oc);
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::TextUnformatted(ssrc!=nullptr?ssrc->name.c_str():"none");
|
||||||
|
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
if (ImGui::SmallButton("->")) {
|
||||||
|
ImGui::OpenPopup("src_connect");
|
||||||
|
}
|
||||||
|
if (ImGui::BeginPopup("src_connect")) {
|
||||||
|
if (ImGui::BeginMenu("connect to")) {
|
||||||
|
for (const auto& [oc_sink, s_sink] : _os.registry().view<Components::StreamSink>().each()) {
|
||||||
|
if (s_sink.frame_type_name != ss.frame_type_name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::PushID(entt::to_integral(oc_sink));
|
||||||
|
|
||||||
|
std::string sink_label {"src "};
|
||||||
|
sink_label += std::to_string(entt::to_integral(entt::to_entity(oc_sink)));
|
||||||
|
sink_label += " (";
|
||||||
|
sink_label += s_sink.name;
|
||||||
|
sink_label += ")[";
|
||||||
|
sink_label += s_sink.frame_type_name;
|
||||||
|
sink_label += "]";
|
||||||
|
if (ImGui::MenuItem(sink_label.c_str())) {
|
||||||
|
_sm.connect(oc, oc_sink);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::TextUnformatted(ssrc!=nullptr?ssrc->frame_type_name.c_str():"???");
|
||||||
|
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndTable();
|
||||||
|
}
|
||||||
|
} // sources header
|
||||||
|
|
||||||
|
if (ImGui::CollapsingHeader("Sinks", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||||
|
// list sinks
|
||||||
|
if (ImGui::BeginTable("sources_and_sinks", 4, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_BordersInnerV)) {
|
||||||
|
ImGui::TableSetupColumn("id");
|
||||||
|
ImGui::TableSetupColumn("name");
|
||||||
|
ImGui::TableSetupColumn("##conn");
|
||||||
|
ImGui::TableSetupColumn("type");
|
||||||
|
|
||||||
|
ImGui::TableHeadersRow();
|
||||||
|
|
||||||
|
for (const auto& [oc, ss] : _os.registry().view<Components::StreamSink>().each()) {
|
||||||
|
ImGui::PushID(entt::to_integral(oc));
|
||||||
|
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::Text("%d", entt::to_integral(entt::to_entity(oc)));
|
||||||
|
|
||||||
|
const auto *ssink = _os.registry().try_get<Components::StreamSink>(oc);
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::TextUnformatted(ssink!=nullptr?ssink->name.c_str():"none");
|
||||||
|
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
if (ImGui::SmallButton("->")) {
|
||||||
|
ImGui::OpenPopup("sink_connect");
|
||||||
|
}
|
||||||
|
// ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings
|
||||||
|
if (ImGui::BeginPopup("sink_connect")) {
|
||||||
|
if (ImGui::BeginMenu("connect to")) {
|
||||||
|
for (const auto& [oc_src, s_src] : _os.registry().view<Components::StreamSource>().each()) {
|
||||||
|
if (s_src.frame_type_name != ss.frame_type_name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::PushID(entt::to_integral(oc_src));
|
||||||
|
|
||||||
|
std::string source_label {"src "};
|
||||||
|
source_label += std::to_string(entt::to_integral(entt::to_entity(oc_src)));
|
||||||
|
source_label += " (";
|
||||||
|
source_label += s_src.name;
|
||||||
|
source_label += ")[";
|
||||||
|
source_label += s_src.frame_type_name;
|
||||||
|
source_label += "]";
|
||||||
|
if (ImGui::MenuItem(source_label.c_str())) {
|
||||||
|
_sm.connect(oc_src, oc);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::TextUnformatted(ssink!=nullptr?ssink->frame_type_name.c_str():"???");
|
||||||
|
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndTable();
|
||||||
|
}
|
||||||
|
} // sink header
|
||||||
|
|
||||||
|
if (ImGui::CollapsingHeader("Connections", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||||
|
// list connections
|
||||||
|
if (ImGui::BeginTable("connections", 6, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_BordersInnerV)) {
|
||||||
|
ImGui::TableSetupColumn("##id"); // TODO: remove?
|
||||||
|
ImGui::TableSetupColumn("##disco");
|
||||||
|
ImGui::TableSetupColumn("##qdesc");
|
||||||
|
ImGui::TableSetupColumn("from");
|
||||||
|
ImGui::TableSetupColumn("to");
|
||||||
|
ImGui::TableSetupColumn("type");
|
||||||
|
|
||||||
|
ImGui::TableHeadersRow();
|
||||||
|
|
||||||
|
for (size_t i = 0; i < _sm._connections.size(); i++) {
|
||||||
|
const auto& con = _sm._connections[i];
|
||||||
|
//ImGui::Text("con %d->%d", entt::to_integral(entt::to_entity(con->src.entity())), entt::to_integral(entt::to_entity(con->sink.entity())));
|
||||||
|
|
||||||
|
ImGui::PushID(i);
|
||||||
|
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::Text("%zu", i); // do connections have ids?
|
||||||
|
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
if (ImGui::SmallButton("X")) {
|
||||||
|
con->stop = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::Text("%d->%d", entt::to_integral(entt::to_entity(con->src.entity())), entt::to_integral(entt::to_entity(con->sink.entity())));
|
||||||
|
|
||||||
|
const auto *ssrc = con->src.try_get<Components::StreamSource>();
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::TextUnformatted(ssrc!=nullptr?ssrc->name.c_str():"none");
|
||||||
|
|
||||||
|
const auto *ssink = con->sink.try_get<Components::StreamSink>();
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::TextUnformatted(ssink!=nullptr?ssink->name.c_str():"none");
|
||||||
|
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::TextUnformatted(
|
||||||
|
(ssrc!=nullptr)?
|
||||||
|
ssrc->frame_type_name.c_str():
|
||||||
|
(ssink!=nullptr)?
|
||||||
|
ssink->frame_type_name.c_str()
|
||||||
|
:"???"
|
||||||
|
);
|
||||||
|
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
ImGui::EndTable();
|
||||||
|
}
|
||||||
|
} // con header
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
17
src/stream_manager_ui.hpp
Normal file
17
src/stream_manager_ui.hpp
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <solanaceae/object_store/fwd.hpp>
|
||||||
|
#include "./stream_manager.hpp"
|
||||||
|
|
||||||
|
class StreamManagerUI {
|
||||||
|
ObjectStore2& _os;
|
||||||
|
StreamManager& _sm;
|
||||||
|
|
||||||
|
bool _show_window {true};
|
||||||
|
|
||||||
|
public:
|
||||||
|
StreamManagerUI(ObjectStore2& os, StreamManager& sm);
|
||||||
|
|
||||||
|
void render(void);
|
||||||
|
};
|
||||||
|
|
169
src/tox_av.cpp
169
src/tox_av.cpp
@ -2,14 +2,85 @@
|
|||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
// https://almogfx.bandcamp.com/track/crushed-w-cassade
|
// https://almogfx.bandcamp.com/track/crushed-w-cassade
|
||||||
|
|
||||||
|
struct ToxAVFriendCallState final {
|
||||||
|
const uint32_t state {TOXAV_FRIEND_CALL_STATE_NONE};
|
||||||
|
|
||||||
|
[[nodiscard]] bool is_error(void) const { return state & TOXAV_FRIEND_CALL_STATE_ERROR; }
|
||||||
|
[[nodiscard]] bool is_finished(void) const { return state & TOXAV_FRIEND_CALL_STATE_FINISHED; }
|
||||||
|
[[nodiscard]] bool is_sending_a(void) const { return state & TOXAV_FRIEND_CALL_STATE_SENDING_A; }
|
||||||
|
[[nodiscard]] bool is_sending_v(void) const { return state & TOXAV_FRIEND_CALL_STATE_SENDING_V; }
|
||||||
|
[[nodiscard]] bool is_accepting_a(void) const { return state & TOXAV_FRIEND_CALL_STATE_ACCEPTING_A; }
|
||||||
|
[[nodiscard]] bool is_accepting_v(void) const { return state & TOXAV_FRIEND_CALL_STATE_ACCEPTING_V; }
|
||||||
|
};
|
||||||
|
|
||||||
ToxAV::ToxAV(Tox* tox) : _tox(tox) {
|
ToxAV::ToxAV(Tox* tox) : _tox(tox) {
|
||||||
Toxav_Err_New err_new {TOXAV_ERR_NEW_OK};
|
Toxav_Err_New err_new {TOXAV_ERR_NEW_OK};
|
||||||
_tox_av = toxav_new(_tox, &err_new);
|
_tox_av = toxav_new(_tox, &err_new);
|
||||||
// TODO: throw
|
// TODO: throw
|
||||||
assert(err_new == TOXAV_ERR_NEW_OK);
|
assert(err_new == TOXAV_ERR_NEW_OK);
|
||||||
|
|
||||||
|
toxav_callback_call(
|
||||||
|
_tox_av,
|
||||||
|
+[](ToxAV*, uint32_t friend_number, bool audio_enabled, bool video_enabled, void *user_data) {
|
||||||
|
assert(user_data != nullptr);
|
||||||
|
static_cast<ToxAV*>(user_data)->cb_call(friend_number, audio_enabled, video_enabled);
|
||||||
|
},
|
||||||
|
this
|
||||||
|
);
|
||||||
|
toxav_callback_call_state(
|
||||||
|
_tox_av,
|
||||||
|
+[](ToxAV*, uint32_t friend_number, uint32_t state, void *user_data) {
|
||||||
|
assert(user_data != nullptr);
|
||||||
|
static_cast<ToxAV*>(user_data)->cb_call_state(friend_number, state);
|
||||||
|
},
|
||||||
|
this
|
||||||
|
);
|
||||||
|
toxav_callback_audio_bit_rate(
|
||||||
|
_tox_av,
|
||||||
|
+[](ToxAV*, uint32_t friend_number, uint32_t audio_bit_rate, void *user_data) {
|
||||||
|
assert(user_data != nullptr);
|
||||||
|
static_cast<ToxAV*>(user_data)->cb_audio_bit_rate(friend_number, audio_bit_rate);
|
||||||
|
},
|
||||||
|
this
|
||||||
|
);
|
||||||
|
toxav_callback_video_bit_rate(
|
||||||
|
_tox_av,
|
||||||
|
+[](ToxAV*, uint32_t friend_number, uint32_t video_bit_rate, void *user_data) {
|
||||||
|
assert(user_data != nullptr);
|
||||||
|
static_cast<ToxAV*>(user_data)->cb_video_bit_rate(friend_number, video_bit_rate);
|
||||||
|
},
|
||||||
|
this
|
||||||
|
);
|
||||||
|
toxav_callback_audio_receive_frame(
|
||||||
|
_tox_av,
|
||||||
|
+[](ToxAV*, uint32_t friend_number, const int16_t pcm[], size_t sample_count, uint8_t channels, uint32_t sampling_rate, void *user_data) {
|
||||||
|
assert(user_data != nullptr);
|
||||||
|
static_cast<ToxAV*>(user_data)->cb_audio_receive_frame(friend_number, pcm, sample_count, channels, sampling_rate);
|
||||||
|
},
|
||||||
|
this
|
||||||
|
);
|
||||||
|
toxav_callback_video_receive_frame(
|
||||||
|
_tox_av,
|
||||||
|
+[](ToxAV*, uint32_t friend_number,
|
||||||
|
uint16_t width, uint16_t height,
|
||||||
|
const uint8_t y[/*! max(width, abs(ystride)) * height */],
|
||||||
|
const uint8_t u[/*! max(width/2, abs(ustride)) * (height/2) */],
|
||||||
|
const uint8_t v[/*! max(width/2, abs(vstride)) * (height/2) */],
|
||||||
|
int32_t ystride, int32_t ustride, int32_t vstride,
|
||||||
|
void *user_data
|
||||||
|
) {
|
||||||
|
assert(user_data != nullptr);
|
||||||
|
static_cast<ToxAV*>(user_data)->cb_video_receive_frame(friend_number, width, height, y, u, v, ystride, ustride, vstride);
|
||||||
|
},
|
||||||
|
this
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ToxAV::~ToxAV(void) {
|
ToxAV::~ToxAV(void) {
|
||||||
toxav_kill(_tox_av);
|
toxav_kill(_tox_av);
|
||||||
}
|
}
|
||||||
@ -80,3 +151,101 @@ Toxav_Err_Bit_Rate_Set ToxAV::toxavVideoSetBitRate(uint32_t friend_number, uint3
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ToxAV::cb_call(uint32_t friend_number, bool audio_enabled, bool video_enabled) {
|
||||||
|
std::cerr << "TOXAV: receiving call f:" << friend_number << " a:" << audio_enabled << " v:" << video_enabled << "\n";
|
||||||
|
//Toxav_Err_Answer err_answer { TOXAV_ERR_ANSWER_OK };
|
||||||
|
//toxav_answer(_tox_av, friend_number, 0, 0, &err_answer);
|
||||||
|
//if (err_answer != TOXAV_ERR_ANSWER_OK) {
|
||||||
|
// std::cerr << "!!!!!!!! answer failed " << err_answer << "\n";
|
||||||
|
//}
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
ToxAV_Event::friend_call,
|
||||||
|
Events::FriendCall{
|
||||||
|
friend_number,
|
||||||
|
audio_enabled,
|
||||||
|
video_enabled,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToxAV::cb_call_state(uint32_t friend_number, uint32_t state) {
|
||||||
|
//ToxAVFriendCallState w_state{state};
|
||||||
|
|
||||||
|
//w_state.is_error();
|
||||||
|
|
||||||
|
std::cerr << "TOXAV: call state f:" << friend_number << " s:" << state << "\n";
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
ToxAV_Event::friend_call_state,
|
||||||
|
Events::FriendCallState{
|
||||||
|
friend_number,
|
||||||
|
state,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToxAV::cb_audio_bit_rate(uint32_t friend_number, uint32_t audio_bit_rate) {
|
||||||
|
std::cerr << "TOXAV: audio bitrate f:" << friend_number << " abr:" << audio_bit_rate << "\n";
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
ToxAV_Event::friend_audio_bitrate,
|
||||||
|
Events::FriendAudioBitrate{
|
||||||
|
friend_number,
|
||||||
|
audio_bit_rate,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToxAV::cb_video_bit_rate(uint32_t friend_number, uint32_t video_bit_rate) {
|
||||||
|
std::cerr << "TOXAV: video bitrate f:" << friend_number << " vbr:" << video_bit_rate << "\n";
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
ToxAV_Event::friend_video_bitrate,
|
||||||
|
Events::FriendVideoBitrate{
|
||||||
|
friend_number,
|
||||||
|
video_bit_rate,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToxAV::cb_audio_receive_frame(uint32_t friend_number, const int16_t pcm[], size_t sample_count, uint8_t channels, uint32_t sampling_rate) {
|
||||||
|
//std::cerr << "TOXAV: audio frame f:" << friend_number << " sc:" << sample_count << " ch:" << (int)channels << " sr:" << sampling_rate << "\n";
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
ToxAV_Event::friend_audio_frame,
|
||||||
|
Events::FriendAudioFrame{
|
||||||
|
friend_number,
|
||||||
|
Span<int16_t>(pcm, sample_count*channels), // TODO: is sample count *ch or /ch?
|
||||||
|
channels,
|
||||||
|
sampling_rate,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ToxAV::cb_video_receive_frame(
|
||||||
|
uint32_t friend_number,
|
||||||
|
uint16_t width, uint16_t height,
|
||||||
|
const uint8_t y[/*! max(width, abs(ystride)) * height */],
|
||||||
|
const uint8_t u[/*! max(width/2, abs(ustride)) * (height/2) */],
|
||||||
|
const uint8_t v[/*! max(width/2, abs(vstride)) * (height/2) */],
|
||||||
|
int32_t ystride, int32_t ustride, int32_t vstride
|
||||||
|
) {
|
||||||
|
//std::cerr << "TOXAV: video frame f:" << friend_number << " w:" << width << " h:" << height << "\n";
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
ToxAV_Event::friend_video_frame,
|
||||||
|
Events::FriendVideoFrame{
|
||||||
|
friend_number,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
Span<uint8_t>(y, std::max<int64_t>(width, std::abs(ystride)) * height),
|
||||||
|
Span<uint8_t>(u, std::max<int64_t>(width/2, std::abs(ustride)) * (height/2)),
|
||||||
|
Span<uint8_t>(v, std::max<int64_t>(width/2, std::abs(vstride)) * (height/2)),
|
||||||
|
ystride,
|
||||||
|
ustride,
|
||||||
|
vstride,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
101
src/tox_av.hpp
101
src/tox_av.hpp
@ -1,15 +1,98 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <solanaceae/util/span.hpp>
|
||||||
|
#include <solanaceae/util/event_provider.hpp>
|
||||||
|
|
||||||
#include <tox/toxav.h>
|
#include <tox/toxav.h>
|
||||||
|
|
||||||
struct ToxAV {
|
namespace /*toxav*/ Events {
|
||||||
|
|
||||||
|
struct FriendCall {
|
||||||
|
uint32_t friend_number;
|
||||||
|
bool audio_enabled;
|
||||||
|
bool video_enabled;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FriendCallState {
|
||||||
|
uint32_t friend_number;
|
||||||
|
uint32_t state;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FriendAudioBitrate {
|
||||||
|
uint32_t friend_number;
|
||||||
|
uint32_t audio_bit_rate;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FriendVideoBitrate {
|
||||||
|
uint32_t friend_number;
|
||||||
|
uint32_t video_bit_rate;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FriendAudioFrame {
|
||||||
|
uint32_t friend_number;
|
||||||
|
|
||||||
|
Span<int16_t> pcm;
|
||||||
|
//size_t sample_count;
|
||||||
|
uint8_t channels;
|
||||||
|
uint32_t sampling_rate;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FriendVideoFrame {
|
||||||
|
uint32_t friend_number;
|
||||||
|
|
||||||
|
uint16_t width;
|
||||||
|
uint16_t height;
|
||||||
|
//const uint8_t y[[>! max(width, abs(ystride)) * height <]];
|
||||||
|
//const uint8_t u[[>! max(width/2, abs(ustride)) * (height/2) <]];
|
||||||
|
//const uint8_t v[[>! max(width/2, abs(vstride)) * (height/2) <]];
|
||||||
|
// mdspan would be nice here
|
||||||
|
// bc of the stride, span might be larger than the actual data it contains
|
||||||
|
Span<uint8_t> y;
|
||||||
|
Span<uint8_t> u;
|
||||||
|
Span<uint8_t> v;
|
||||||
|
int32_t ystride;
|
||||||
|
int32_t ustride;
|
||||||
|
int32_t vstride;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // Event
|
||||||
|
|
||||||
|
enum class ToxAV_Event : uint32_t {
|
||||||
|
friend_call,
|
||||||
|
friend_call_state,
|
||||||
|
friend_audio_bitrate,
|
||||||
|
friend_video_bitrate,
|
||||||
|
friend_audio_frame,
|
||||||
|
friend_video_frame,
|
||||||
|
|
||||||
|
MAX
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ToxAVEventI {
|
||||||
|
using enumType = ToxAV_Event;
|
||||||
|
|
||||||
|
virtual ~ToxAVEventI(void) {}
|
||||||
|
|
||||||
|
virtual bool onEvent(const Events::FriendCall&) { return false; }
|
||||||
|
virtual bool onEvent(const Events::FriendCallState&) { return false; }
|
||||||
|
virtual bool onEvent(const Events::FriendAudioBitrate&) { return false; }
|
||||||
|
virtual bool onEvent(const Events::FriendVideoBitrate&) { return false; }
|
||||||
|
virtual bool onEvent(const Events::FriendAudioFrame&) { return false; }
|
||||||
|
virtual bool onEvent(const Events::FriendVideoFrame&) { return false; }
|
||||||
|
};
|
||||||
|
using ToxAVEventProviderI = EventProviderI<ToxAVEventI>;
|
||||||
|
|
||||||
|
struct ToxAV : public ToxAVEventProviderI{
|
||||||
Tox* _tox = nullptr;
|
Tox* _tox = nullptr;
|
||||||
ToxAV* _tox_av = nullptr;
|
ToxAV* _tox_av = nullptr;
|
||||||
|
|
||||||
|
static constexpr const char* version {"0"};
|
||||||
|
|
||||||
ToxAV(Tox* tox);
|
ToxAV(Tox* tox);
|
||||||
virtual ~ToxAV(void);
|
virtual ~ToxAV(void);
|
||||||
|
|
||||||
// interface
|
// interface
|
||||||
|
// if iterate is called on a different thread, it will fire events there
|
||||||
uint32_t toxavIterationInterval(void) const;
|
uint32_t toxavIterationInterval(void) const;
|
||||||
void toxavIterate(void);
|
void toxavIterate(void);
|
||||||
|
|
||||||
@ -33,5 +116,21 @@ struct ToxAV {
|
|||||||
//int32_t toxav_groupchat_disable_av(Tox *tox, uint32_t groupnumber);
|
//int32_t toxav_groupchat_disable_av(Tox *tox, uint32_t groupnumber);
|
||||||
//bool toxav_groupchat_av_enabled(Tox *tox, uint32_t groupnumber);
|
//bool toxav_groupchat_av_enabled(Tox *tox, uint32_t groupnumber);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// toxav callbacks
|
||||||
|
void cb_call(uint32_t friend_number, bool audio_enabled, bool video_enabled);
|
||||||
|
void cb_call_state(uint32_t friend_number, uint32_t state);
|
||||||
|
void cb_audio_bit_rate(uint32_t friend_number, uint32_t audio_bit_rate);
|
||||||
|
void cb_video_bit_rate(uint32_t friend_number, uint32_t video_bit_rate);
|
||||||
|
void cb_audio_receive_frame(uint32_t friend_number, const int16_t pcm[], size_t sample_count, uint8_t channels, uint32_t sampling_rate);
|
||||||
|
void cb_video_receive_frame(
|
||||||
|
uint32_t friend_number,
|
||||||
|
uint16_t width, uint16_t height,
|
||||||
|
const uint8_t y[/*! max(width, abs(ystride)) * height */],
|
||||||
|
const uint8_t u[/*! max(width/2, abs(ustride)) * (height/2) */],
|
||||||
|
const uint8_t v[/*! max(width/2, abs(vstride)) * (height/2) */],
|
||||||
|
int32_t ystride, int32_t ustride, int32_t vstride
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user