Skip to main content

mpv_client_cross/
lib.rs

1#![allow(non_upper_case_globals)]
2#![allow(non_camel_case_types)]
3#![allow(non_snake_case)]
4
5mod error;
6mod format;
7pub mod node;
8
9pub use error::{Error, Result};
10pub use format::Format;
11pub use node::Node;
12
13use std::ffi::{CStr, CString, c_char, c_void};
14use std::fmt;
15use std::mem::MaybeUninit;
16use std::ops::{Deref, DerefMut};
17use std::ptr::slice_from_raw_parts_mut;
18
19pub use ffi::mpv_handle;
20use ffi::*;
21
22use crate::node::from_mpv_node;
23
24/// Representation of a borrowed client context used by the client API.
25/// Every client has its own private handle.
26pub struct Handle {
27    inner: [mpv_handle],
28}
29
30/// A type representing an owned client context.
31pub struct Client(*mut mpv_handle);
32
33/// An enum representing the available events that can be received by
34/// `Handle::wait_event`.
35pub enum Event {
36    /// Nothing happened. Happens on timeouts or sporadic wakeups.
37    None,
38    /// Happens when the player quits. The player enters a state where it tries
39    /// to disconnect all clients.
40    Shutdown,
41    /// See `Handle::request_log_messages`.
42    /// See also `LogMessage`.
43    LogMessage(LogMessage),
44    /// Reply to a `Handle::get_property_async` request.
45    /// See also `Property`.
46    GetPropertyReply(Result<()>, u64, Property),
47    /// Reply to a `Handle::set_property_async` request.
48    /// (Unlike `GetPropertyReply`, `Property` is not used.)
49    SetPropertyReply(Result<()>, u64),
50    /// Reply to a `Handle::command_async` or mpv_command_node_async() request.
51    /// See also `Command`.
52    CommandReply(Result<()>, u64), // TODO mpv_event_command and mpv_node
53    /// Notification before playback start of a file (before the file is loaded).
54    /// See also `StartFile`.
55    StartFile(StartFile),
56    /// Notification after playback end (after the file was unloaded).
57    /// See also `EndFile`.
58    EndFile(EndFile),
59    /// Notification when the file has been loaded (headers were read etc.), and
60    /// decoding starts.
61    FileLoaded,
62    /// Triggered by the script-message input command. The command uses the
63    /// first argument of the command as client name (see `Handle::client_name`) to
64    /// dispatch the message, and passes along all arguments starting from the
65    /// second argument as strings.
66    /// See also `ClientMessage`.
67    ClientMessage(ClientMessage),
68    /// Happens after video changed in some way. This can happen on resolution
69    /// changes, pixel format changes, or video filter changes. The event is
70    /// sent after the video filters and the VO are reconfigured. Applications
71    /// embedding a mpv window should listen to this event in order to resize
72    /// the window if needed.
73    /// Note that this event can happen sporadically, and you should check
74    /// yourself whether the video parameters really changed before doing
75    /// something expensive.
76    VideoReconfig,
77    /// Similar to `VideoReconfig`. This is relatively uninteresting,
78    /// because there is no such thing as audio output embedding.
79    AudioReconfig,
80    /// Happens when a seek was initiated. Playback stops. Usually it will
81    /// resume with `PlaybackRestart` as soon as the seek is finished.
82    Seek,
83    /// There was a discontinuity of some sort (like a seek), and playback
84    /// was reinitialized. Usually happens on start of playback and after
85    /// seeking. The main purpose is allowing the client to detect when a seek
86    /// request is finished.
87    PlaybackRestart,
88    /// Event sent due to `mpv_observe_property()`.
89    /// See also `Property`.
90    PropertyChange(u64, Property),
91    /// Happens if the internal per-mpv_handle ringbuffer overflows, and at
92    /// least 1 event had to be dropped. This can happen if the client doesn't
93    /// read the event queue quickly enough with `Handle::wait_event`, or if the
94    /// client makes a very large number of asynchronous calls at once.
95    ///
96    /// Event delivery will continue normally once this event was returned
97    /// (this forces the client to empty the queue completely).
98    QueueOverflow,
99    /// Triggered if a hook handler was registered with `Handle::hook_add`, and the
100    /// hook is invoked. If you receive this, you must handle it, and continue
101    /// the hook with `Handle::hook_continue`.
102    /// See also `Hook`.
103    Hook(u64, Hook),
104}
105
106/// Data associated with `Event::GetPropertyReply` and `Event::PropertyChange`.
107pub struct Property(*const mpv_event_property);
108
109/// Data associated with `Event::LogMessage`.
110#[allow(dead_code)]
111pub struct LogMessage(*const mpv_event_log_message);
112
113/// Data associated with `Event::StartFile`.
114pub struct StartFile(*const mpv_event_start_file);
115
116/// Data associated with `Event::EndFile`.
117#[allow(dead_code)]
118pub struct EndFile(*const mpv_event_end_file);
119
120/// Data associated with `Event::ClientMessage`.
121pub struct ClientMessage(*const mpv_event_client_message);
122
123/// Data associated with `Event::Hook`.
124pub struct Hook(*const mpv_event_hook);
125
126macro_rules! result {
127    ($f:expr) => {
128        match $f {
129            mpv_error_MPV_ERROR_SUCCESS => Ok(()),
130            e => Err(Error::new(e)),
131        }
132    };
133}
134
135macro_rules! result_with_code {
136    ($f:expr) => {
137        if $f >= mpv_error_MPV_ERROR_SUCCESS {
138            Ok($f)
139        } else {
140            Err(Error::new($f))
141        }
142    };
143}
144
145#[macro_export]
146macro_rules! osd {
147    ($client:expr, $duration:expr, $($arg:tt)*) => {
148        $client.command(&["show-text", &format!($($arg)*), &$duration.as_millis().to_string()])
149    }
150}
151
152#[macro_export]
153macro_rules! osd_async {
154    ($client:expr, $reply:expr, $duration:expr, $($arg:tt)*) => {
155        $client.command_async($reply, &["show-text", &format!($($arg)*), &$duration.as_millis().to_string()])
156    }
157}
158
159impl Handle {
160    /// Wrap a raw mpv_handle
161    ///
162    /// This function will wrap the provided `ptr` with a `Handle` wrapper, which
163    /// allows inspection and interoperation of non-owned `mpv_handle`.
164    ///
165    /// # Safety
166    ///
167    /// * `ptr` must be non null.
168    ///
169    /// * The memory referenced by the returned `Handle` must not be freed for
170    ///   the duration of lifetime `'a`.
171    #[inline]
172    pub fn from_ptr<'a>(ptr: *mut mpv_handle) -> &'a mut Self {
173        unsafe { &mut *(slice_from_raw_parts_mut(ptr, 1) as *mut Self) }
174    }
175
176    /// # Safety
177    ///
178    /// `Handle` must have been created from a valid, non-null `mpv_handle` pointer.
179    #[inline]
180    pub unsafe fn as_ptr(&self) -> *const mpv_handle {
181        self.inner.as_ptr()
182    }
183
184    /// # Safety
185    ///
186    /// `Handle` must have been created from a valid, non-null `mpv_handle` pointer,
187    /// and there must be no other active mutable references to the underlying handle.
188    #[inline]
189    pub unsafe fn as_mut_ptr(&mut self) -> *mut mpv_handle {
190        self.inner.as_mut_ptr()
191    }
192
193    pub fn create_client(&mut self, name: impl AsRef<str>) -> Result<Client> {
194        let name = CString::new(name.as_ref())?;
195        let handle = unsafe { mpv_create_client(self.as_mut_ptr(), name.as_ptr()) };
196        if handle.is_null() {
197            Err(Error::new(mpv_error_MPV_ERROR_NOMEM))
198        } else {
199            Ok(Client(handle))
200        }
201    }
202
203    pub fn create_weak_client(&mut self, name: impl AsRef<str>) -> Result<Client> {
204        let name = CString::new(name.as_ref())?;
205        let handle = unsafe { mpv_create_weak_client(self.as_mut_ptr(), name.as_ptr()) };
206        if handle.is_null() {
207            Err(Error::new(mpv_error_MPV_ERROR_NOMEM))
208        } else {
209            Ok(Client(handle))
210        }
211    }
212
213    pub fn initialize(&mut self) -> Result<()> {
214        unsafe { result!(mpv_initialize(self.as_mut_ptr())) }
215    }
216
217    /// Wait for the next event, or until the timeout expires, or if another thread
218    /// makes a call to `mpv_wakeup()`. Passing 0 as timeout will never wait, and
219    /// is suitable for polling.
220    ///
221    /// The internal event queue has a limited size (per client handle). If you
222    /// don't empty the event queue quickly enough with `Handle::wait_event`, it will
223    /// overflow and silently discard further events. If this happens, making
224    /// asynchronous requests will fail as well (with MPV_ERROR_EVENT_QUEUE_FULL).
225    ///
226    /// Only one thread is allowed to call this on the same `Handle` at a time.
227    /// The API won't complain if more than one thread calls this, but it will cause
228    /// race conditions in the client when accessing the shared mpv_event struct.
229    /// Note that most other API functions are not restricted by this, and no API
230    /// function internally calls `mpv_wait_event()`. Additionally, concurrent calls
231    /// to different handles are always safe.
232    ///
233    /// As long as the timeout is 0, this is safe to be called from mpv render API
234    /// threads.
235    pub fn wait_event(&mut self, timeout: f64) -> Event {
236        unsafe { Event::from_ptr(mpv_wait_event(self.as_mut_ptr(), timeout)) }
237    }
238
239    /// Return the name of this client handle. Every client has its own unique
240    /// name, which is mostly used for user interface purposes.
241    pub fn name<'a>(&mut self) -> &'a str {
242        unsafe {
243            CStr::from_ptr(mpv_client_name(self.as_mut_ptr()))
244                .to_str()
245                .unwrap_or("unknown")
246        }
247    }
248
249    /// Return the ID of this client handle. Every client has its own unique ID. This
250    /// ID is never reused by the core, even if the mpv_handle at hand gets destroyed
251    /// and new handles get allocated.
252    ///
253    /// IDs are never 0 or negative.
254    ///
255    /// Some mpv APIs (not necessarily all) accept a name in the form "@<id>" in
256    /// addition of the proper mpv_client_name(), where "<id>" is the ID in decimal
257    /// form (e.g. "@123"). For example, the "script-message-to" command takes the
258    /// client name as first argument, but also accepts the client ID formatted in
259    /// this manner.
260    #[inline]
261    pub fn id(&mut self) -> i64 {
262        unsafe { mpv_client_id(self.as_mut_ptr()) }
263    }
264
265    /// Send a command to the player. Commands are the same as those used in
266    /// input.conf, except that this function takes parameters in a pre-split
267    /// form.
268    pub fn command<I, S>(&mut self, args: I) -> Result<()>
269    where
270        I: IntoIterator<Item = S>,
271        S: AsRef<str>,
272    {
273        let args: Vec<CString> = args.into_iter().map(|s| CString::new(s.as_ref()).unwrap()).collect();
274        let mut raw_args: Vec<*const c_char> = args.iter().map(|s| s.as_ptr()).collect();
275        raw_args.push(std::ptr::null()); // Adding null at the end
276        unsafe { result!(mpv_command(self.as_mut_ptr(), raw_args.as_mut_ptr())) }
277    }
278
279    pub fn command_ret<I, S>(&mut self, args: I) -> Result<Node>
280    where
281        I: IntoIterator<Item = S>,
282        S: AsRef<str>,
283    {
284        let args: Vec<CString> = args.into_iter().map(|s| CString::new(s.as_ref()).unwrap()).collect();
285        let mut raw_args: Vec<*const c_char> = args.iter().map(|s| s.as_ptr()).collect();
286        raw_args.push(std::ptr::null()); // Adding null at the end
287
288        let mut res = MaybeUninit::<mpv_node>::zeroed();
289        let ret = unsafe { mpv_command_ret(self.as_mut_ptr(), raw_args.as_mut_ptr(), res.as_mut_ptr()) };
290
291        result!(ret)?;
292        unsafe { Ok(from_mpv_node(res.assume_init_mut())) }
293    }
294
295    /// Same as `Handle::command`, but run the command asynchronously.
296    ///
297    /// Commands are executed asynchronously. You will receive a
298    /// `CommandReply` event. This event will also have an
299    /// error code set if running the command failed. For commands that
300    /// return data, the data is put into mpv_event_command.result.
301    ///
302    /// The only case when you do not receive an event is when the function call
303    /// itself fails. This happens only if parsing the command itself (or otherwise
304    /// validating it) fails, i.e. the return code of the API call is not 0 or
305    /// positive.
306    ///
307    /// Safe to be called from mpv render API threads.
308    pub fn command_async<I, S>(&mut self, reply: u64, args: I) -> Result<()>
309    where
310        I: IntoIterator<Item = S>,
311        S: AsRef<str>,
312    {
313        let args: Vec<CString> = args.into_iter().map(|s| CString::new(s.as_ref()).unwrap()).collect();
314        let mut raw_args: Vec<*const c_char> = args.iter().map(|s| s.as_ptr()).collect();
315        raw_args.push(std::ptr::null()); // Adding null at the end
316        unsafe { result!(mpv_command_async(self.as_mut_ptr(), reply, raw_args.as_mut_ptr())) }
317    }
318
319    pub fn set_property<T: Format>(&mut self, name: impl AsRef<str>, data: T) -> Result<()> {
320        let name = CString::new(name.as_ref())?;
321        let handle = unsafe { self.as_mut_ptr() };
322        data.to_mpv(|data| unsafe { result!(mpv_set_property(handle, name.as_ptr(), T::MPV_FORMAT, data)) })
323    }
324
325    /// Read the value of the given property.
326    ///
327    /// If the format doesn't match with the internal format of the property, access
328    /// usually will fail with `MPV_ERROR_PROPERTY_FORMAT`. In some cases, the data
329    /// is automatically converted and access succeeds. For example, i64 is always
330    /// converted to f64, and access using String usually invokes a string formatter.
331    pub fn get_property<T: Format>(&mut self, name: impl AsRef<str>) -> Result<T> {
332        let name = CString::new(name.as_ref())?;
333        let handle = unsafe { self.as_mut_ptr() };
334        T::from_mpv(|data| unsafe { result!(mpv_get_property(handle, name.as_ptr(), T::MPV_FORMAT, data)) })
335    }
336
337    pub fn observe_property<T: Format>(&mut self, reply: u64, name: impl AsRef<str>) -> Result<()> {
338        let name = CString::new(name.as_ref())?;
339        unsafe {
340            result!(mpv_observe_property(
341                self.as_mut_ptr(),
342                reply,
343                name.as_ptr(),
344                T::MPV_FORMAT
345            ))
346        }
347    }
348
349    /// Undo `Handle::observe_property`. This will remove all observed properties for
350    /// which the given number was passed as reply to `Handle::observe_property`.
351    ///
352    /// Safe to be called from mpv render API threads.
353    pub fn unobserve_property(&mut self, registered_reply: u64) -> Result<i32> {
354        unsafe { result_with_code!(mpv_unobserve_property(self.as_mut_ptr(), registered_reply)) }
355    }
356
357    pub fn hook_add(&mut self, reply: u64, name: &str, priority: i32) -> Result<()> {
358        let name = CString::new(name)?;
359        unsafe { result!(mpv_hook_add(self.as_mut_ptr(), reply, name.as_ptr(), priority)) }
360    }
361
362    pub fn hook_continue(&mut self, id: u64) -> Result<()> {
363        unsafe { result!(mpv_hook_continue(self.as_mut_ptr(), id)) }
364    }
365}
366
367impl Client {
368    pub fn new() -> Result<Self> {
369        let handle = unsafe { mpv_create() };
370        if handle.is_null() {
371            Err(Error::new(mpv_error_MPV_ERROR_NOMEM))
372        } else {
373            Ok(Self(handle))
374        }
375    }
376
377    pub fn initialize(self) -> Result<Self> {
378        unsafe { result!(mpv_initialize(self.0)).map(|()| self) }
379    }
380}
381
382impl Drop for Client {
383    fn drop(&mut self) {
384        unsafe { mpv_destroy(self.0) }
385    }
386}
387
388impl Deref for Client {
389    type Target = Handle;
390
391    #[inline]
392    fn deref(&self) -> &Self::Target {
393        Handle::from_ptr(self.0)
394    }
395}
396
397impl DerefMut for Client {
398    #[inline]
399    fn deref_mut(&mut self) -> &mut Self::Target {
400        Handle::from_ptr(self.0)
401    }
402}
403
404unsafe impl Send for Client {}
405
406impl Event {
407    unsafe fn from_ptr(event: *const mpv_event) -> Event {
408        unsafe {
409            match (*event).event_id {
410                mpv_event_id_MPV_EVENT_SHUTDOWN => Event::Shutdown,
411                mpv_event_id_MPV_EVENT_LOG_MESSAGE => Event::LogMessage(LogMessage::from_ptr((*event).data)),
412                mpv_event_id_MPV_EVENT_GET_PROPERTY_REPLY => Event::GetPropertyReply(
413                    result!((*event).error),
414                    (*event).reply_userdata,
415                    Property::from_ptr((*event).data),
416                ),
417                mpv_event_id_MPV_EVENT_SET_PROPERTY_REPLY => {
418                    Event::SetPropertyReply(result!((*event).error), (*event).reply_userdata)
419                }
420                mpv_event_id_MPV_EVENT_COMMAND_REPLY => {
421                    Event::CommandReply(result!((*event).error), (*event).reply_userdata)
422                }
423                mpv_event_id_MPV_EVENT_START_FILE => Event::StartFile(StartFile::from_ptr((*event).data)),
424                mpv_event_id_MPV_EVENT_END_FILE => Event::EndFile(EndFile::from_ptr((*event).data)),
425                mpv_event_id_MPV_EVENT_FILE_LOADED => Event::FileLoaded,
426                mpv_event_id_MPV_EVENT_CLIENT_MESSAGE => Event::ClientMessage(ClientMessage::from_ptr((*event).data)),
427                mpv_event_id_MPV_EVENT_VIDEO_RECONFIG => Event::VideoReconfig,
428                mpv_event_id_MPV_EVENT_AUDIO_RECONFIG => Event::AudioReconfig,
429                mpv_event_id_MPV_EVENT_SEEK => Event::Seek,
430                mpv_event_id_MPV_EVENT_PLAYBACK_RESTART => Event::PlaybackRestart,
431                mpv_event_id_MPV_EVENT_PROPERTY_CHANGE => {
432                    Event::PropertyChange((*event).reply_userdata, Property::from_ptr((*event).data))
433                }
434                mpv_event_id_MPV_EVENT_QUEUE_OVERFLOW => Event::QueueOverflow,
435                mpv_event_id_MPV_EVENT_HOOK => Event::Hook((*event).reply_userdata, Hook::from_ptr((*event).data)),
436                _ => Event::None,
437            }
438        }
439    }
440}
441
442impl fmt::Display for Event {
443    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
444        let event = match *self {
445            Self::Shutdown => mpv_event_id_MPV_EVENT_SHUTDOWN,
446            Self::LogMessage(..) => mpv_event_id_MPV_EVENT_LOG_MESSAGE,
447            Self::GetPropertyReply(..) => mpv_event_id_MPV_EVENT_GET_PROPERTY_REPLY,
448            Self::SetPropertyReply(..) => mpv_event_id_MPV_EVENT_SET_PROPERTY_REPLY,
449            Self::CommandReply(..) => mpv_event_id_MPV_EVENT_COMMAND_REPLY,
450            Self::StartFile(..) => mpv_event_id_MPV_EVENT_START_FILE,
451            Self::EndFile(..) => mpv_event_id_MPV_EVENT_END_FILE,
452            Self::FileLoaded => mpv_event_id_MPV_EVENT_FILE_LOADED,
453            Self::ClientMessage(..) => mpv_event_id_MPV_EVENT_CLIENT_MESSAGE,
454            Self::VideoReconfig => mpv_event_id_MPV_EVENT_VIDEO_RECONFIG,
455            Self::AudioReconfig => mpv_event_id_MPV_EVENT_AUDIO_RECONFIG,
456            Self::Seek => mpv_event_id_MPV_EVENT_SEEK,
457            Self::PlaybackRestart => mpv_event_id_MPV_EVENT_PLAYBACK_RESTART,
458            Self::PropertyChange(..) => mpv_event_id_MPV_EVENT_PROPERTY_CHANGE,
459            Self::QueueOverflow => mpv_event_id_MPV_EVENT_QUEUE_OVERFLOW,
460            Self::Hook(..) => mpv_event_id_MPV_EVENT_HOOK,
461            _ => mpv_event_id_MPV_EVENT_NONE,
462        };
463
464        f.write_str(unsafe {
465            CStr::from_ptr(mpv_event_name(event))
466                .to_str()
467                .unwrap_or("unknown event")
468        })
469    }
470}
471
472impl Property {
473    /// Wrap a raw mpv_event_property
474    /// The pointer must not be null
475    fn from_ptr(ptr: *const c_void) -> Self {
476        assert!(!ptr.is_null());
477        Self(ptr as *const mpv_event_property)
478    }
479
480    /// Name of the property.
481    pub fn name(&self) -> &str {
482        unsafe { CStr::from_ptr((*self.0).name) }.to_str().unwrap_or("unknown")
483    }
484
485    pub fn data<T: Format>(&self) -> Option<T> {
486        unsafe {
487            if (*self.0).format == T::MPV_FORMAT {
488                T::from_ptr((*self.0).data).ok()
489            } else {
490                None
491            }
492        }
493    }
494}
495
496impl fmt::Display for Property {
497    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
498        f.write_str(self.name())
499    }
500}
501
502impl LogMessage {
503    /// Wrap a raw mpv_event_log_message
504    /// The pointer must not be null
505    fn from_ptr(ptr: *const c_void) -> Self {
506        assert!(!ptr.is_null());
507        Self(ptr as *const mpv_event_log_message)
508    }
509}
510
511impl fmt::Display for LogMessage {
512    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
513        f.write_str("log message")
514    }
515}
516
517impl StartFile {
518    /// Wrap a raw mpv_event_start_file
519    /// The pointer must not be null
520    fn from_ptr(ptr: *const c_void) -> Self {
521        assert!(!ptr.is_null());
522        Self(ptr as *const mpv_event_start_file)
523    }
524
525    /// Playlist entry ID of the file being loaded now.
526    pub fn playlist_entry_id(&self) -> i64 {
527        unsafe { (*self.0).playlist_entry_id }
528    }
529}
530
531impl fmt::Display for StartFile {
532    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
533        f.write_str("start file")
534    }
535}
536
537impl EndFile {
538    /// Wrap a raw mpv_event_end_file
539    /// The pointer must not be null
540    fn from_ptr(ptr: *const c_void) -> Self {
541        assert!(!ptr.is_null());
542        Self(ptr as *const mpv_event_end_file)
543    }
544}
545
546impl fmt::Display for EndFile {
547    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
548        f.write_str("end file")
549    }
550}
551
552impl ClientMessage {
553    /// Wrap a raw mpv_event_client_message.
554    /// The pointer must not be null
555    fn from_ptr(ptr: *const c_void) -> Self {
556        assert!(!ptr.is_null());
557        Self(ptr as *const mpv_event_client_message)
558    }
559
560    pub fn args<'a>(&self) -> Vec<&'a str> {
561        unsafe {
562            let args = std::slice::from_raw_parts((*self.0).args, (*self.0).num_args as usize);
563            args.iter().map(|arg| CStr::from_ptr(*arg).to_str().unwrap()).collect()
564        }
565    }
566}
567
568impl fmt::Display for ClientMessage {
569    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
570        f.write_str("client-message")
571    }
572}
573
574impl Hook {
575    /// Wrap a raw mpv_event_hook.
576    /// The pointer must not be null
577    fn from_ptr(ptr: *const c_void) -> Self {
578        assert!(!ptr.is_null());
579        Self(ptr as *const mpv_event_hook)
580    }
581
582    /// The hook name as passed to `Handle::hook_add`.
583    pub fn name(&self) -> &str {
584        unsafe { CStr::from_ptr((*self.0).name).to_str().unwrap_or("unknown") }
585    }
586
587    /// Internal ID that must be passed to `Handle::hook_continue`.
588    pub fn id(&self) -> u64 {
589        unsafe { (*self.0).id }
590    }
591}
592
593impl fmt::Display for Hook {
594    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
595        f.write_str(self.name())
596    }
597}