Compare commits
	
		
			34 Commits
		
	
	
		
			776d376232
			...
			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 (${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU" OR ${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") | ||||
| 		if (NOT WIN32) # exclude mingw | ||||
| 			#link_libraries(-fsanitize=address) | ||||
| 			add_compile_options(-fsanitize=address,undefined) | ||||
| 			link_libraries(-fsanitize=address,undefined) | ||||
| 			#link_libraries(-fsanitize=undefined) | ||||
| 			link_libraries(-static-libasan) # make it "work" on nix | ||||
|  | ||||
| 			#add_compile_options(-fsanitize=thread) | ||||
| 			#link_libraries(-fsanitize=thread) | ||||
|  | ||||
| 			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() | ||||
| 			message("!! can not enable ASAN on this platform (gcc/clang + win)") | ||||
| 		endif() | ||||
|   | ||||
							
								
								
									
										1
									
								
								external/CMakeLists.txt
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								external/CMakeLists.txt
									
									
									
									
										vendored
									
									
								
							| @@ -24,3 +24,4 @@ add_subdirectory(./libwebp) | ||||
| add_subdirectory(./qoi) | ||||
| 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; | ||||
|  | ||||
|         cmakeFlags = [ | ||||
|           "-DTOMATO_TOX_AV=ON" | ||||
|  | ||||
|           "-DTOMATO_ASAN=OFF" | ||||
|           "-DCMAKE_BUILD_TYPE=RelWithDebInfo" | ||||
|           #"-DCMAKE_BUILD_TYPE=Debug" | ||||
|   | ||||
| @@ -102,12 +102,30 @@ target_sources(tomato PUBLIC | ||||
|  | ||||
| 	./chat_gui4.hpp | ||||
| 	./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) | ||||
| 	target_sources(tomato PUBLIC | ||||
| 		./tox_av.hpp | ||||
| 		./tox_av.cpp | ||||
|  | ||||
| 		./debug_tox_call.hpp | ||||
| 		./debug_tox_call.cpp | ||||
| 	) | ||||
|  | ||||
| 	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) | ||||
| 	qoi | ||||
| 	SDL3_image::SDL3_image | ||||
|  | ||||
| 	SPSCQueue | ||||
| ) | ||||
|  | ||||
| # 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 "./content/sdl_video_frame_stream2.hpp" | ||||
| #include "./content/sdl_audio_frame_stream2.hpp" | ||||
|  | ||||
| #include <filesystem> | ||||
| #include <memory> | ||||
| #include <iostream> | ||||
| @@ -35,7 +38,7 @@ int main(int argc, char** argv) { | ||||
|  | ||||
| 	// setup hints | ||||
| #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"; | ||||
| 	} | ||||
| #endif | ||||
| @@ -72,10 +75,51 @@ int main(int argc, char** argv) { | ||||
|  | ||||
| 	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::CreateContext(); | ||||
|  | ||||
| 	// TODO: test android behaviour | ||||
| 	// -> its too big, dpi does not take eye-screen-distance into account | ||||
| 	float display_scale = SDL_GetWindowDisplayScale(window.get()); | ||||
| 	if (display_scale < 0.001f) { | ||||
| 		// error? | ||||
|   | ||||
| @@ -9,6 +9,11 @@ | ||||
|  | ||||
| #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 <cmath> | ||||
| #include <string_view> | ||||
| @@ -19,11 +24,13 @@ MainScreen::MainScreen(SimpleConfigModel&& conf_, SDL_Renderer* renderer_, Theme | ||||
| 	rmm(cr), | ||||
| 	msnj{cr, {}, {}}, | ||||
| 	mts(rmm), | ||||
| 	sm(os), | ||||
| 	tc(save_path, save_password), | ||||
| 	tpi(tc.getTox()), | ||||
| 	ad(tc), | ||||
| #if TOMATO_TOX_AV | ||||
| 	tav(tc.getTox()), | ||||
| 	dtc(os, tav, sdlrtu), | ||||
| #endif | ||||
| 	tcm(cr, 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), | ||||
| 	sw(conf), | ||||
| 	osui(os), | ||||
| 	smui(os, sm), | ||||
| 	dvt(os, sm, sdlrtu), | ||||
| 	tuiu(tc, conf), | ||||
| 	tdch(tpi) | ||||
| { | ||||
| @@ -136,6 +145,53 @@ MainScreen::MainScreen(SimpleConfigModel&& conf_, SDL_Renderer* renderer_, Theme | ||||
| 	} | ||||
|  | ||||
| 	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) { | ||||
| @@ -252,14 +308,19 @@ Screen* MainScreen::render(float time_delta, bool&) { | ||||
| 	} | ||||
| 	// ACTUALLY NOT IF RENDERED, MOVED LOGIC TO ABOVE | ||||
| 	// it might unload textures, so it needs to be done before rendering | ||||
| 	const float ctc_interval = contact_tc.update(); | ||||
| 	const float msgtc_interval = msg_tc.update(); | ||||
| 	float animation_interval = contact_tc.update(); | ||||
| 	animation_interval = std::min<float>(animation_interval, msg_tc.update()); | ||||
|  | ||||
| 	const float cg_interval = cg.render(time_delta); // render | ||||
| 	sw.render(); // render | ||||
| 	osui.render(); | ||||
| 	smui.render(); | ||||
| 	animation_interval = std::min<float>(animation_interval, dvt.render()); | ||||
| 	tuiu.render(); // render | ||||
| 	tdch.render(); // render | ||||
| #if TOMATO_TOX_AV | ||||
| 	animation_interval = std::min<float>(animation_interval, dtc.render()); | ||||
| #endif | ||||
|  | ||||
| 	{ // main window menubar injection | ||||
| 		if (ImGui::Begin("tomato")) { | ||||
| @@ -440,8 +501,7 @@ Screen* MainScreen::render(float time_delta, bool&) { | ||||
|  | ||||
| 	// low delay time 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, msgtc_interval); | ||||
| 		_render_interval = std::min<float>(_render_interval, animation_interval); | ||||
|  | ||||
| 		_render_interval = std::clamp( | ||||
| 			_render_interval, | ||||
| @@ -450,8 +510,7 @@ Screen* MainScreen::render(float time_delta, bool&) { | ||||
| 		); | ||||
| 	// mid delay time 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, msgtc_interval); | ||||
| 		_render_interval = std::min<float>(_render_interval, animation_interval); | ||||
|  | ||||
| 		_render_interval = std::clamp( | ||||
| 			_render_interval, | ||||
| @@ -476,8 +535,16 @@ Screen* MainScreen::render(float time_delta, bool&) { | ||||
| Screen* MainScreen::tick(float time_delta, bool& quit) { | ||||
| 	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 | ||||
|  | ||||
| 	const float sm_interval = sm.tick(time_delta); | ||||
|  | ||||
| 	const float fo_interval = tffom.tick(time_delta); | ||||
|  | ||||
| 	tam.iterate(); // compute | ||||
| @@ -510,6 +577,18 @@ Screen* MainScreen::tick(float time_delta, bool& quit) { | ||||
| 		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"; | ||||
|  | ||||
| 	switch (_compute_perf_mode) { | ||||
|   | ||||
| @@ -16,6 +16,8 @@ | ||||
| #include <solanaceae/tox_messages/tox_message_manager.hpp> | ||||
| #include <solanaceae/tox_messages/tox_transfer_manager.hpp> | ||||
|  | ||||
| #include "./stream_manager.hpp" | ||||
|  | ||||
| #include "./tox_client.hpp" | ||||
| #include "./auto_dirty.hpp" | ||||
|  | ||||
| @@ -30,12 +32,15 @@ | ||||
| #include "./chat_gui4.hpp" | ||||
| #include "./chat_gui/settings_window.hpp" | ||||
| #include "./object_store_ui.hpp" | ||||
| #include "./stream_manager_ui.hpp" | ||||
| #include "./debug_video_tap.hpp" | ||||
| #include "./tox_ui_utils.hpp" | ||||
| #include "./tox_dht_cap_histo.hpp" | ||||
| #include "./tox_friend_faux_offline_messaging.hpp" | ||||
|  | ||||
| #if TOMATO_TOX_AV | ||||
| #include "./tox_av.hpp" | ||||
| #include "./debug_tox_call.hpp" | ||||
| #endif | ||||
|  | ||||
| #include <string> | ||||
| @@ -58,12 +63,15 @@ struct MainScreen final : public Screen { | ||||
| 	MessageSerializerNJ msnj; | ||||
| 	MessageTimeSort mts; | ||||
|  | ||||
| 	StreamManager sm; | ||||
|  | ||||
| 	ToxEventLogger tel{std::cout}; | ||||
| 	ToxClient tc; | ||||
| 	ToxPrivateImpl tpi; | ||||
| 	AutoDirty ad; | ||||
| #if TOMATO_TOX_AV | ||||
| 	ToxAV tav; | ||||
| 	DebugToxCall dtc; | ||||
| #endif | ||||
| 	ToxContactModel2 tcm; | ||||
| 	ToxMessageManager tmm; | ||||
| @@ -86,6 +94,8 @@ struct MainScreen final : public Screen { | ||||
| 	ChatGui4 cg; | ||||
| 	SettingsWindow sw; | ||||
| 	ObjectStoreUI osui; | ||||
| 	StreamManagerUI smui; | ||||
| 	DebugVideoTap dvt; | ||||
| 	ToxUIUtils tuiu; | ||||
| 	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_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); | ||||
| 	} | ||||
|  | ||||
|   | ||||
							
								
								
									
										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 <cstdint> | ||||
| #include <iostream> | ||||
|  | ||||
| // 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_Err_New err_new {TOXAV_ERR_NEW_OK}; | ||||
| 	_tox_av = toxav_new(_tox, &err_new); | ||||
| 	// TODO: throw | ||||
| 	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_kill(_tox_av); | ||||
| } | ||||
| @@ -80,3 +151,101 @@ Toxav_Err_Bit_Rate_Set ToxAV::toxavVideoSetBitRate(uint32_t friend_number, uint3 | ||||
| 	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 | ||||
|  | ||||
| #include <solanaceae/util/span.hpp> | ||||
| #include <solanaceae/util/event_provider.hpp> | ||||
|  | ||||
| #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; | ||||
| 	ToxAV* _tox_av = nullptr; | ||||
|  | ||||
| 	static constexpr const char* version {"0"}; | ||||
|  | ||||
| 	ToxAV(Tox* tox); | ||||
| 	virtual ~ToxAV(void); | ||||
|  | ||||
| 	// interface | ||||
| 	// if iterate is called on a different thread, it will fire events there | ||||
| 	uint32_t toxavIterationInterval(void) const; | ||||
| 	void toxavIterate(void); | ||||
|  | ||||
| @@ -33,5 +116,21 @@ struct ToxAV { | ||||
| //int32_t toxav_groupchat_disable_av(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 | ||||
| 	); | ||||
| }; | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user